Passed
Push — master ( edd7fd...017cdc )
by Yannick
15:03 queued 06:38
created

DownloadSelectedDocumentsAction   A

Complexity

Total Complexity 21

Size/Duplication

Total Lines 126
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 66
c 1
b 0
f 0
dl 0
loc 126
rs 10
wmc 21

4 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
B __invoke() 0 47 9
A addNodeToZip() 0 23 6
A createZipFile() 0 32 5
1
<?php
2
3
declare(strict_types=1);
4
5
/* For licensing terms, see /license.txt */
6
7
namespace Chamilo\CoreBundle\Controller\Api;
8
9
use Chamilo\CoreBundle\Entity\ResourceNode;
10
use Chamilo\CourseBundle\Entity\CDocument;
11
use Doctrine\ORM\EntityManagerInterface;
12
use Exception;
13
use Symfony\Component\HttpFoundation\Request;
14
use Symfony\Component\HttpFoundation\Response;
15
use Symfony\Component\HttpFoundation\StreamedResponse;
16
use Symfony\Component\HttpKernel\KernelInterface;
17
use Chamilo\CoreBundle\Repository\ResourceNodeRepository;
18
use ZipArchive;
19
20
class DownloadSelectedDocumentsAction
21
{
22
    private KernelInterface $kernel;
23
    private ResourceNodeRepository $resourceNodeRepository;
24
25
    public function __construct(KernelInterface $kernel, ResourceNodeRepository $resourceNodeRepository)
26
    {
27
        $this->kernel = $kernel;
28
        $this->resourceNodeRepository = $resourceNodeRepository;
29
    }
30
31
    public function __invoke(Request $request, EntityManagerInterface $em): Response
32
    {
33
        ini_set('max_execution_time', '300');
34
        ini_set('memory_limit', '512M');
35
36
        $data = json_decode($request->getContent(), true);
37
        $documentIds = $data['ids'] ?? [];
38
39
        if (empty($documentIds)) {
40
            return new Response('No items selected.', Response::HTTP_BAD_REQUEST);
41
        }
42
43
        $documents = $em->getRepository(CDocument::class)->findBy(['iid' => $documentIds]);
44
45
        if (empty($documents)) {
46
            return new Response('No documents found.', Response::HTTP_NOT_FOUND);
47
        }
48
49
        $zipFilePath = $this->createZipFile($documents);
50
51
        if (!$zipFilePath || !file_exists($zipFilePath)) {
52
            return new Response('ZIP file not found or could not be created.', Response::HTTP_INTERNAL_SERVER_ERROR);
53
        }
54
55
        $fileSize = filesize($zipFilePath);
56
        if ($fileSize === false || $fileSize === 0) {
57
            error_log('ZIP file is empty or unreadable.');
58
            throw new Exception('ZIP file is empty or unreadable.');
59
        }
60
61
        $response = new StreamedResponse(function () use ($zipFilePath) {
62
            $handle = fopen($zipFilePath, 'rb');
63
            if ($handle) {
0 ignored issues
show
introduced by
$handle is of type resource, thus it always evaluated to false.
Loading history...
64
                while (!feof($handle)) {
65
                    echo fread($handle, 8192);
66
                    ob_flush();
67
                    flush();
68
                }
69
                fclose($handle);
70
            }
71
        });
72
73
        $response->headers->set('Content-Type', 'application/zip');
74
        $response->headers->set('Content-Disposition', 'inline; filename="selected_documents.zip"');
75
        $response->headers->set('Content-Length', (string) $fileSize);
76
77
        return $response;
78
    }
79
80
    /**
81
     * Creates a ZIP file containing the specified documents.
82
     *
83
     * @return string The path to the created ZIP file.
84
     * @throws Exception If the ZIP file cannot be created or closed.
85
     */
86
    private function createZipFile(array $documents): string
87
    {
88
        $cacheDir = $this->kernel->getCacheDir();
89
        $zipFilePath = $cacheDir . '/selected_documents_' . uniqid() . '.zip';
90
91
        $zip = new ZipArchive();
92
        $result = $zip->open($zipFilePath, ZipArchive::CREATE);
93
94
        if ($result !== true) {
95
            throw new Exception('Unable to create ZIP file');
96
        }
97
98
        $projectDir = $this->kernel->getProjectDir();
99
        $baseUploadDir = $projectDir . '/var/upload/resource';
100
101
        foreach ($documents as $document) {
102
            $resourceNode = $document->getResourceNode();
103
            if (!$resourceNode) {
104
                error_log('ResourceNode not found for document ID: ' . $document->getId());
105
                continue;
106
            }
107
108
            $this->addNodeToZip($zip, $resourceNode, $baseUploadDir);
109
        }
110
111
        if (!$zip->close()) {
112
            error_log('Failed to close ZIP file.');
113
            throw new Exception('Failed to close ZIP archive');
114
        }
115
116
117
        return $zipFilePath;
118
    }
119
120
    /**
121
     * Adds a resource node and its files or children to the ZIP archive.
122
     */
123
    private function addNodeToZip(ZipArchive $zip, ResourceNode $node, string $baseUploadDir, string $currentPath = ''): void
124
    {
125
126
        if ($node->getChildren()->count() > 0) {
127
            $relativePath = $currentPath . $node->getTitle() . '/';
128
            $zip->addEmptyDir($relativePath);
129
130
            foreach ($node->getChildren() as $childNode) {
131
                $this->addNodeToZip($zip, $childNode, $baseUploadDir, $relativePath);
132
            }
133
        } elseif ($node->hasResourceFile()) {
134
            foreach ($node->getResourceFiles() as $resourceFile) {
135
                $filePath = $baseUploadDir . $this->resourceNodeRepository->getFilename($resourceFile);
136
                $fileName = $currentPath . $resourceFile->getOriginalName();
137
138
                if (file_exists($filePath)) {
139
                    $zip->addFile($filePath, $fileName);
140
                } else {
141
                    error_log('File not found: ' . $filePath);
142
                }
143
            }
144
        } else {
145
            error_log('Node has no children or files: ' . $node->getTitle());
146
        }
147
    }
148
}
149