Test Failed
Push — master ( 1352e7...0da4e2 )
by Esteban De La Fuente
03:30
created

File::send()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 20
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
cc 4
eloc 12
c 0
b 0
f 0
nc 5
nop 2
dl 0
loc 20
ccs 0
cts 13
cp 0
crap 20
rs 9.8666
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * Derafu: Biblioteca PHP (Núcleo).
7
 * Copyright (C) Derafu <https://www.derafu.org>
8
 *
9
 * Este programa es software libre: usted puede redistribuirlo y/o modificarlo
10
 * bajo los términos de la Licencia Pública General Affero de GNU publicada por
11
 * la Fundación para el Software Libre, ya sea la versión 3 de la Licencia, o
12
 * (a su elección) cualquier versión posterior de la misma.
13
 *
14
 * Este programa se distribuye con la esperanza de que sea útil, pero SIN
15
 * GARANTÍA ALGUNA; ni siquiera la garantía implícita MERCANTIL o de APTITUD
16
 * PARA UN PROPÓSITO DETERMINADO. Consulte los detalles de la Licencia Pública
17
 * General Affero de GNU para obtener una información más detallada.
18
 *
19
 * Debería haber recibido una copia de la Licencia Pública General Affero de GNU
20
 * junto a este programa.
21
 *
22
 * En caso contrario, consulte <http://www.gnu.org/licenses/agpl.html>.
23
 */
24
25
namespace Derafu\Lib\Core\Helper;
26
27
use RecursiveDirectoryIterator;
28
use RecursiveIteratorIterator;
29
use RuntimeException;
30
use Symfony\Component\Filesystem\Filesystem;
31
use Symfony\Component\Mime\MimeTypes;
32
use ZipStream\OperationMode;
33
use ZipStream\ZipStream;
34
35
/**
36
 * Clase para trabajar con archivos.
37
 */
38
class File
39
{
40
    /**
41
     * Borra recursivamente un directorio.
42
     *
43
     * @param string $dir Directorio a borrar.
44
     * @return void
45
     */
46
    public static function rmdir(string $dir): void
47
    {
48
        $filesystem = new Filesystem();
49
        $filesystem->remove($dir);
50
    }
51
52
    /**
53
     * Entrega el mimetype de un archivo.
54
     *
55
     * @param string $file Ruta hacia el fichero.
56
     * @return string|false Mimetype del fichero o `false` si no se pudo
57
     * determinar.
58
     */
59
    public static function mimetype(string $file): string|false
60
    {
61
        if (!file_exists($file)) {
62
            return false;
63
        }
64
65
        $mimeTypes = new MimeTypes();
66
        return $mimeTypes->guessMimeType($file) ?: false;
67
    }
68
69
    /**
70
     * Empaqueta y comprime archivos o directorios en un archivo ZIP.
71
     *
72
     * @param string $file Directorio o archivo que se desea comprimir.
73
     * @param bool $download Indica si se debe enviar el archivo comprimido a
74
     * través del navegador. Si se solicita descargar, el archivo descargado se
75
     * eliminará luego de ser enviado por el navegador siempre. Para controlar
76
     * esto usar compress() y luego send() (por separado).
77
     * @param bool $delete Indica si se debe eliminar el archivo o directorio
78
     * original que se comprimió luego de ser procesado.
79
     * @return void
80
     */
81
    public static function compress(
82
        string $file,
83
        bool $download = false,
84
        bool $delete = false
85
    ): void {
86
        if (!is_readable($file)) {
87
            throw new RuntimeException(sprintf(
88
                'No se puede leer el archivo %s que se desea comprimir.',
89
                $file
90
            ));
91
        }
92
93
        $zipFilePath = $file . '.zip';
94
95
        self::zip($file, $zipFilePath);
96
97
        if ($download) {
98
            self::send($zipFilePath, true);
99
        }
100
101
        if ($delete) {
102
            self::rmdir($file);
103
        }
104
    }
105
106
    /**
107
     * Comprime un archivo o directorio usando ZipStream.
108
     *
109
     * Este método utiliza la biblioteca `ZipStream` para comprimir el archivo
110
     * o directorio especificado en un archivo ZIP. En caso de error durante la
111
     * compresión, las excepciones lanzadas por `ZipStream` se propagarán sin
112
     * ser capturadas.
113
     *
114
     * @param string $file Ruta del archivo o directorio a comprimir.
115
     * @param string $zipFilePath Nombre del archivo comprimido resultante.
116
     * @return void
117
     *
118
     * @throws RuntimeException Si no se puede abrir el archivo para escritura.
119
     */
120
    public static function zip(string $file, string $zipFilePath): void
121
    {
122
        // Abre un flujo para el archivo de salida.
123
        $outputStream = fopen($zipFilePath, 'w');
124
        if ($outputStream === false) {
125
            throw new RuntimeException(sprintf(
126
                'No se puede abrir el archivo para escritura: $%s',
127
                $zipFilePath
128
            ));
129
        }
130
131
        try {
132
            // Crear instancia de ZipStream con modo de operación NORMAL.
133
            $zip = new ZipStream(
134
                operationMode: OperationMode::NORMAL,
135
                outputStream: $outputStream,
136
                sendHttpHeaders: false,
137
            );
138
139
            // Si es un directorio, agregar todos los archivos de forma
140
            // recursiva.
141
            if (is_dir($file)) {
142
                $finder = new RecursiveIteratorIterator(
143
                    new RecursiveDirectoryIterator($file)
144
                );
145
                foreach ($finder as $foundFile) {
146
                    if (!$foundFile->isDir()) {
147
                        $localName = str_replace(
148
                            $file . DIRECTORY_SEPARATOR,
149
                            '',
150
                            $foundFile->getPathname()
151
                        );
152
                        $zip->addFileFromPath(
153
                            $localName,
154
                            $foundFile->getPathname()
155
                        );
156
                    }
157
                }
158
            } else {
159
                // Si es un archivo, agregarlo directamente.
160
                $zip->addFileFromPath(basename($file), $file);
161
            }
162
163
            // Finaliza la creación del archivo zip.
164
            $zip->finish();
165
        } finally {
166
            // Cierra el flujo de salida.
167
            fclose($outputStream);
168
        }
169
    }
170
171
    /**
172
     * Envía un archivo para su descarga a través del navegador.
173
     *
174
     * @param string $file
175
     * @return void
176
     */
177
    private static function send(string $file, bool $delete = false): void
178
    {
179
        if (!file_exists($file)) {
180
            return;
181
        }
182
183
        $mimetype = self::mimetype($file);
184
        if ($mimetype) {
185
            header('Content-Type: ' . $mimetype);
186
        }
187
188
        header('Content-Disposition: attachment; filename="' . basename($file) . '"');
189
        header('Content-Length: ' . filesize($file));
190
        header('Pragma: no-cache');
191
        header('Expires: 0');
192
193
        readfile($file);
194
195
        if ($delete) {
196
            unlink($file);
197
        }
198
    }
199
}
200