Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 116
0.00% covered (danger)
0.00%
0 / 20
CRAP
0.00% covered (danger)
0.00%
0 / 1
DeliveryArchiveService
0.00% covered (danger)
0.00%
0 / 116
0.00% covered (danger)
0.00%
0 / 20
2070
0.00% covered (danger)
0.00%
0 / 1
 catchDeliveryCreated
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 catchDeliveryRemoved
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 archive
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
42
 unArchive
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
30
 deleteArchive
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 deleteDeliveryData
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 copyFromZip
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
30
 uploadZip
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 getArchiveFileSystem
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 download
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
 getArchiveFileName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getLocalZipPathName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTmpPath
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 generateNewTmpPath
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 deleteTmpFile
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 isZipDirectory
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getUniqueProcessedName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setArchiveProcessed
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 refreshArchiveProcessed
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 isArchivedProcessed
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3/**
4 * This program is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU General Public License
6 * as published by the Free Software Foundation; under version 2
7 * of the License (non-upgradable).
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17 *
18 * Copyright (c) 2017 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT);
19 *
20 */
21
22namespace oat\taoDeliveryRdf\model;
23
24use common_Logger;
25use oat\generis\model\OntologyAwareTrait;
26use oat\oatbox\filesystem\Directory;
27use oat\oatbox\filesystem\File;
28use oat\oatbox\filesystem\FileSystem;
29use oat\oatbox\filesystem\FileSystemService;
30use oat\oatbox\service\ConfigurableService;
31use oat\oatbox\service\ServiceNotFoundException;
32use oat\taoDeliveryRdf\model\Delete\DeliveryDeleteRequest;
33use oat\taoDeliveryRdf\model\event\DeliveryCreatedEvent;
34use oat\taoDeliveryRdf\model\event\DeliveryRemovedEvent;
35use tao_models_classes_service_FileStorage;
36use tao_models_classes_service_StorageDirectory;
37use oat\taoDeliveryRdf\model\Delete\DeliveryDelete;
38
39class DeliveryArchiveService extends ConfigurableService implements DeliveryArchiveServiceInterface, DeliveryDelete
40{
41    use OntologyAwareTrait;
42
43    public const BUCKET_DIRECTORY = 'deliveriesArchives';
44
45    /** @var string */
46    protected $tmpDir;
47
48    /**
49     * @param DeliveryCreatedEvent $event
50     * @throws ServiceNotFoundException
51     */
52    public function catchDeliveryCreated(DeliveryCreatedEvent $event)
53    {
54        $compiledDelivery = $this->getResource($event->getDeliveryUri());
55
56        try {
57            $this->archive($compiledDelivery);
58        } catch (DeliverArchiveExistingException $e) {
59            common_Logger::i($e->getMessage());
60        } catch (DeliveryZipException $e) {
61            common_Logger::e($e->getMessage());
62        }
63    }
64
65    /**
66     * @param DeliveryRemovedEvent $event
67     * @throws ServiceNotFoundException
68     */
69    public function catchDeliveryRemoved(DeliveryRemovedEvent $event)
70    {
71        $compiledDelivery = $this->getResource($event->getDeliveryUri());
72
73        $this->deleteArchive($compiledDelivery);
74    }
75
76    /**
77     * @param \core_kernel_classes_Resource $compiledDelivery
78     * @param bool $force
79     * @return string
80     * @throws DeliverArchiveExistingException
81     * @throws DeliveryZipException
82     */
83    public function archive($compiledDelivery, $force = false)
84    {
85        $fileName = $this->getArchiveFileName($compiledDelivery);
86
87        if (!$force && $this->getArchiveFileSystem()->fileExists($fileName)) {
88            throw new DeliverArchiveExistingException(
89                'Delivery archive already created: ' . $compiledDelivery->getUri()
90            );
91        }
92
93        $this->generateNewTmpPath($fileName);
94        $localZipName = $this->getLocalZipPathName($fileName);
95
96        $zip = new \ZipArchive();
97        if (($errorCode = $zip->open($localZipName, \ZipArchive::CREATE)) !== true) {
98            throw new DeliveryZipException('Cannot open zip archive: ' . $localZipName . ' error code: ' . $errorCode);
99        }
100
101        $directories = $compiledDelivery->getPropertyValues(
102            $this->getProperty(DeliveryAssemblyService::PROPERTY_DELIVERY_DIRECTORY)
103        );
104        foreach ($directories as $directoryId) {
105            /** @var tao_models_classes_service_StorageDirectory $directory */
106            $directory = $this
107                ->getServiceLocator()
108                ->get(tao_models_classes_service_FileStorage::SERVICE_ID)
109                ->getDirectoryById($directoryId);
110            $directories = $directory->getFlyIterator(Directory::ITERATOR_FILE | Directory::ITERATOR_RECURSIVE);
111            /** @var File $item */
112            foreach ($directories as $item) {
113                $zip->addFromString($item->getFileSystemId() . '/' . $item->getPrefix(), $item->read());
114            }
115        }
116        $zip = $this->refreshArchiveProcessed($zip);
117        $zip->close();
118
119        $fileName = $this->uploadZip($compiledDelivery);
120
121        $this->deleteTmpFile($localZipName);
122
123        return $fileName;
124    }
125
126    /**
127     * @param \core_kernel_classes_Resource $compiledDelivery
128     * @param bool $force
129     * @return string
130     * @throws DeliveryArchiveNotExistingException
131     * @throws ServiceNotFoundException
132     * @throws DeliveryZipException
133     */
134    public function unArchive($compiledDelivery, $force = false)
135    {
136        $fileName = $this->getArchiveFileName($compiledDelivery);
137
138        if (!$this->getArchiveFileSystem()->fileExists($fileName)) {
139            throw new DeliveryArchiveNotExistingException(
140                'Delivery archive not exist please generate: ' . $compiledDelivery->getUri()
141            );
142        }
143
144        $this->generateNewTmpPath($fileName);
145        $zipPath = $this->download($compiledDelivery);
146
147        $zip = new \ZipArchive();
148        if (($errorCode = $zip->open($zipPath, \ZipArchive::CREATE)) !== true) {
149            throw new DeliveryZipException('Cannot open zip archive: ' . $zipPath . ' error code: ' . $errorCode);
150        }
151
152        if ($force || !$this->isArchivedProcessed($zip, $fileName)) {
153            $this->copyFromZip($zip);
154            $this->setArchiveProcessed($zip, $fileName);
155            $zip->close();
156
157            $fileName = $this->uploadZip($compiledDelivery);
158        } else {
159            $zip->close();
160        }
161
162        $this->deleteTmpFile($zipPath);
163
164        return $fileName;
165    }
166
167    /**
168     * @param \core_kernel_classes_Resource $compiledDelivery
169     * @return string
170     * @throws ServiceNotFoundException
171     */
172    public function deleteArchive($compiledDelivery)
173    {
174        $fileName = $this->getArchiveFileName($compiledDelivery);
175        if ($this->getArchiveFileSystem()->fileExists($fileName)) {
176            $this->getArchiveFileSystem()->delete($fileName);
177        }
178
179        return $fileName;
180    }
181
182    /**
183     * @inheritdoc
184     */
185    public function deleteDeliveryData(DeliveryDeleteRequest $request)
186    {
187        return $this->deleteArchive($request->getDeliveryResource());
188    }
189
190    /**
191     * @param $zip \ZipArchive
192     * @return bool
193     * @throws ServiceNotFoundException
194     */
195    private function copyFromZip($zip)
196    {
197        /** @var FileSystemService $fileSystem */
198        $fileSystem = $this->getServiceLocator()->get(FileSystemService::SERVICE_ID);
199
200        for ($index = 0; $index < $zip->numFiles; ++$index) {
201            $zipEntryName = $zip->getNameIndex($index);
202            if (!$this->isZipDirectory($zipEntryName)) {
203                $parts = explode('/', $zipEntryName);
204                $bucketDestination = $parts[0];
205                unset($parts[0]);
206                if (in_array($bucketDestination, ['public', 'private',])) {
207                    $entryName = implode('/', $parts);
208                    $stream = $zip->getStream($zipEntryName);
209                    if (is_resource($stream)) {
210                        $fileSystem->getFileSystem($bucketDestination)->writeStream($entryName, $stream);
211                    }
212                }
213            }
214        }
215        return true;
216    }
217
218    /**
219     * @param \core_kernel_classes_Resource $compiledDelivery
220     * @return string
221     * @throws ServiceNotFoundException
222     * @throws DeliveryZipException
223     */
224    private function uploadZip($compiledDelivery)
225    {
226        $fileName = $this->getArchiveFileName($compiledDelivery);
227        $zipPath = $this->getLocalZipPathName($fileName);
228
229        if (!file_exists($zipPath) || ($stream = fopen($zipPath, 'r')) === false) {
230            throw new DeliveryZipException('Cannot open local tmp zip archive');
231        }
232        $this->getArchiveFileSystem()->writeStream($fileName, $stream);
233        fclose($stream);
234
235        return $fileName;
236    }
237
238    /**
239     * @return FileSystem
240     * @throws ServiceNotFoundException
241     */
242    private function getArchiveFileSystem()
243    {
244        return $this->getServiceLocator()->get(FileSystemService::SERVICE_ID)->getFileSystem(static::BUCKET_DIRECTORY);
245    }
246
247    /**
248     * @param \core_kernel_classes_Resource $compiledDelivery
249     * @return string
250     * @throws DeliveryZipException
251     */
252    private function download($compiledDelivery)
253    {
254        $fileName = $this->getArchiveFileName($compiledDelivery);
255        $zipPath = $this->getLocalZipPathName($fileName);
256
257        $streamDestination = fopen($zipPath, 'w');
258        $stream = $this->getArchiveFileSystem()->readStream($fileName);
259        if ($stream === false) {
260            throw new DeliveryZipException('Cannot open zip archive');
261        }
262
263        if ($streamDestination === false) {
264            throw new DeliveryZipException('Cannot write to local tmp zip archive');
265        }
266
267        stream_copy_to_stream($stream, $streamDestination);
268        fclose($stream);
269        fclose($streamDestination);
270
271        return $zipPath;
272    }
273
274    /**
275     * @param \core_kernel_classes_Resource $compiledDelivery
276     * @return string
277     */
278    private function getArchiveFileName($compiledDelivery)
279    {
280        return md5($compiledDelivery->getUri()) . '.zip';
281    }
282
283    /**
284     * @param $fileName
285     * @return string
286     */
287    private function getLocalZipPathName($fileName)
288    {
289        return $this->getTmpPath() . $fileName;
290    }
291
292    /**
293     * @return mixed
294     */
295    private function getTmpPath()
296    {
297        return $this->tmpDir;
298    }
299
300
301    /**
302     * generate unique tmp folder based on delivery.
303     * @param $fileName
304     */
305    private function generateNewTmpPath($fileName)
306    {
307        $folder = sys_get_temp_dir() . DIRECTORY_SEPARATOR . "tmp"
308            . md5($fileName . uniqid('', true)) . DIRECTORY_SEPARATOR;
309
310        if (!file_exists($folder)) {
311            mkdir($folder);
312        }
313
314        $this->tmpDir = $folder;
315    }
316
317    /**
318     * @param $tmpZipPath
319     */
320    private function deleteTmpFile($tmpZipPath)
321    {
322        unlink($tmpZipPath);
323        if (\helpers_File::emptyDirectory($this->tmpDir)) {
324            rmdir($this->tmpDir);
325        }
326    }
327
328    /**
329     * @param $zipEntryName
330     * @return bool
331     */
332    private function isZipDirectory($zipEntryName)
333    {
334        return substr($zipEntryName, -1) ===  '/';
335    }
336
337    /**
338     * @param $fileName
339     * @return string
340     */
341    private function getUniqueProcessedName($fileName)
342    {
343        return md5(gethostname()) . $fileName;
344    }
345
346    /**
347     * @param \ZipArchive $zip
348     * @param $fileName
349     * @return \ZipArchive
350     */
351    private function setArchiveProcessed($zip, $fileName)
352    {
353        $stats = json_decode($zip->getArchiveComment(), true);
354        if (is_null($stats)) {
355            $stats = ['processed' => []];
356        }
357
358        $stats['processed'][] = $this->getUniqueProcessedName($fileName);
359        $zip->setArchiveComment(json_encode($stats));
360
361        return $zip;
362    }
363
364    /**
365     * @param \ZipArchive $zip
366     * @return \ZipArchive
367     */
368    private function refreshArchiveProcessed($zip)
369    {
370        $stats = ['processed' => []];
371        $zip->setArchiveComment(json_encode($stats));
372
373        return $zip;
374    }
375
376    /**
377     * @param \ZipArchive $zip
378     * @return bool
379     */
380    private function isArchivedProcessed($zip, $fileName)
381    {
382        $stats = json_decode($zip->getArchiveComment(), true);
383        if (is_null($stats) || !isset($stats['processed'])) {
384            $stats = ['processed' => []];
385        }
386
387        return in_array($this->getUniqueProcessedName($fileName), $stats['processed']);
388    }
389}