Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 113
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 / 113
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 / 29
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
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(): int
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((string) $file->file)));
85                $this->info("      🗑️  LUEGO SE ELIMINARÍA EL BLOB DE LA BD");
86            }
87
88            $this->info('');
89        }
90    }
91
92    private function executeOptimizedMigration(): int
93    {
94        $this->info('🚀 Iniciando migración/limpieza optimizada...');
95
96        // Obtener IDs en lugar de objetos completos para ahorrar memoria
97        $fileIds = TblFiles::whereNotNull('filename')
98            ->whereNotNull('file') // Buscamos los que TIENEN blob
99            ->pluck('file_id');
100
101        $this->info('📁 Archivos a procesar: '.number_format($fileIds->count()));
102
103        $successCount = 0;
104        $generatedCount = 0;
105        $errorCount = 0;
106
107        $chunks = $fileIds->chunk($this->option('chunk'));
108
109        foreach ($chunks as $chunkIndex => $chunk) {
110            $this->info('🔄 Chunk '.($chunkIndex + 1).'/'.$chunks->count().
111                ' - Memoria: '.$this->formatBytes(memory_get_usage(true)));
112
113            foreach ($chunk as $fileId) {
114                try {
115                    $result = $this->migrateFileWithLowMemory($fileId);
116
117                    if ($result === 'cleaned') {
118                        $successCount++;
119                    } elseif ($result === 'generated_and_cleaned') {
120                        $generatedCount++;
121                    } else {
122                        $errorCount++;
123                    }
124
125                } catch (Exception $e) {
126                    $errorCount++;
127                    $this->error("❌ ID {$fileId}".$e->getMessage());
128                }
129
130                // Liberar memoria después de cada archivo
131                gc_collect_cycles();
132            }
133
134            $this->info("   ✅ Progreso: {$successCount} limpiados (existían), {$generatedCount} generados, {$errorCount} errores");
135
136            // Pausa más larga entre chunks
137            sleep(1);
138        }
139
140        $this->info("🎉 Proceso completado: {$successCount} limpiados, {$generatedCount} generados y limpiados, {$errorCount} errores");
141
142        return 0;
143    }
144
145    private function migrateFileWithLowMemory($fileId): string
146    {
147        // 1. Verificar si existe físico primero (sin traer el blob aún)
148        $fileInfo = TblFiles::where('file_id', $fileId)
149            ->select('file_id', 'filename')
150            ->first();
151
152        if (! $fileInfo) {
153            return 'error';
154        }
155
156        $filePath = storage_path('app/public/uploads/'.$fileInfo->filename);
157        $directory = dirname($filePath);
158
159        // Si existe el archivo físico
160        if (File::exists($filePath)) {
161            // Solo limpiamos el blob
162            DB::table('tbl_files')
163                ->where('file_id', $fileId)
164                ->update(['file' => null]);
165
166            return 'cleaned';
167        }
168
169        // Si NO existe, necesitamos traer el blob para crearlo
170        $fileWithBlob = TblFiles::where('file_id', $fileId)
171            ->select('file')
172            ->first();
173
174        if (! $fileWithBlob || empty($fileWithBlob->file)) {
175            $this->error("❌ ID {$fileId}: No tiene archivo físico NI blob válido.");
176
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) {
189            $this->error("❌ ID {$fileId}: No se pudo escribir el archivo en disco.");
190
191            return 'error';
192        }
193
194        // Verificar que se escribió correctamente
195        if (file_exists($filePath) && filesize($filePath) > 0) {
196            // Limpiar el blob
197            DB::table('tbl_files')
198                ->where('file_id', $fileId)
199                ->update(['file' => null]);
200
201            $this->info("   ✨ Generado: {$fileInfo->filename}");
202
203            return 'generated_and_cleaned';
204        }
205
206        return 'error';
207    }
208
209    private function readFileInChunks($filePath): string
210    {
211        $content = '';
212        $handle = fopen($filePath, 'rb');
213
214        if ($handle) {
215            while (!feof($handle)) {
216                $content .= fread($handle, 8192); // 8KB chunks
217
218                // Liberar memoria periódicamente
219                if (strlen($content) > (5 * 1024 * 1024)) { // Cada 5MB
220                    gc_collect_cycles();
221                }
222            }
223            fclose($handle);
224        }
225
226        return $content;
227    }
228
229    private function formatBytes($bytes): string
230    {
231        $units = ['B', 'KB', 'MB', 'GB', 'TB'];
232        $bytes = max($bytes, 0);
233        $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
234        $pow = min($pow, count($units) - 1);
235        $bytes /= 1024 ** $pow;
236
237        return round($bytes, 2).' '.$units[$pow];
238    }
239}