Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 105
0.00% covered (danger)
0.00%
0 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
MigrateFilesToBlob
0.00% covered (danger)
0.00%
0 / 105
0.00% covered (danger)
0.00%
0 / 5
702
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
 formatBytes
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3// app/Console/Commands/MigrateFilesToBlob.php
4
5namespace App\Console\Commands;
6
7use App\Models\TblFiles;
8use Exception;
9use Illuminate\Console\Command;
10use Illuminate\Support\Facades\DB;
11use Illuminate\Support\Facades\File;
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
144        return 0;
145    }
146
147    private function migrateFileWithLowMemory($fileId)
148    {
149        // 1. Verificar si existe físico primero (sin traer el blob aún)
150        $fileInfo = TblFiles::where('file_id', $fileId)
151            ->select('file_id', 'filename')
152            ->first();
153
154        if (! $fileInfo) {
155            return 'error';
156        }
157
158        $filePath = storage_path('app/public/uploads/'.$fileInfo->filename);
159        $directory = dirname($filePath);
160
161        // Si existe el archivo físico
162        if (File::exists($filePath)) {
163            // Solo limpiamos el blob
164            DB::table('tbl_files')
165                ->where('file_id', $fileId)
166                ->update(['file' => null]);
167
168            return 'cleaned';
169        }
170
171        // Si NO existe, necesitamos traer el blob para crearlo
172        $fileWithBlob = TblFiles::where('file_id', $fileId)
173            ->select('file')
174            ->first();
175
176        if (! $fileWithBlob || empty($fileWithBlob->file)) {
177            $this->error("❌ ID {$fileId}: No tiene archivo físico NI blob válido.");
178
179            return 'error';
180        }
181
182        // Asegurar que el directorio existe
183        if (! File::exists($directory)) {
184            File::makeDirectory($directory, 0755, true);
185        }
186
187        // Escribir el archivo
188        $written = File::put($filePath, $fileWithBlob->file);
189
190        if (! $written) {
191            $this->error("❌ ID {$fileId}: No se pudo escribir el archivo en disco.");
192
193            return 'error';
194        }
195
196        // Verificar que se escribió correctamente
197        if (file_exists($filePath) && filesize($filePath) > 0) {
198            // Limpiar el blob
199            DB::table('tbl_files')
200                ->where('file_id', $fileId)
201                ->update(['file' => null]);
202
203            $this->info("   ✨ Generado: {$fileInfo->filename}");
204
205            return 'generated_and_cleaned';
206        }
207
208        return 'error';
209    }
210
211    private function formatBytes($bytes): string
212    {
213        $units = ['B', 'KB', 'MB', 'GB', 'TB'];
214        $bytes = max($bytes, 0);
215        $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
216        $pow = min($pow, count($units) - 1);
217        $bytes /= pow(1024, $pow);
218
219        return round($bytes, 2).' '.$units[$pow];
220    }
221}