MDL-37429 zipping improvements
[moodle.git] / lib / filestorage / zip_packer.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle 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 Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * Implementation of zip packer.
19  *
20  * @package   core_files
21  * @copyright 2008 Petr Skoda (http://skodak.org)
22  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 defined('MOODLE_INTERNAL') || die();
27 require_once("$CFG->libdir/filestorage/file_packer.php");
28 require_once("$CFG->libdir/filestorage/zip_archive.php");
30 /**
31  * Utility class - handles all zipping and unzipping operations.
32  *
33  * @package   core_files
34  * @category  files
35  * @copyright 2008 Petr Skoda (http://skodak.org)
36  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
37  */
38 class zip_packer extends file_packer {
40     /**
41      * Zip files and store the result in file storage.
42      *
43      * @param array $files array with full zip paths (including directory information)
44      *              as keys (archivepath=>ospathname or archivepath/subdir=>stored_file or archivepath=>array('content_as_string'))
45      * @param int $contextid context ID
46      * @param string $component component
47      * @param string $filearea file area
48      * @param int $itemid item ID
49      * @param string $filepath file path
50      * @param string $filename file name
51      * @param int $userid user ID
52      * @param bool $ignoreinvalidfiles true means ignore missing or invalid files, false means abort on any error
53      * @return stored_file|bool false if error stored_file instance if ok
54      */
55     public function archive_to_storage(array $files, $contextid, $component, $filearea, $itemid, $filepath, $filename, $userid = NULL, $ignoreinvalidfiles=true) {
56         global $CFG;
58         $fs = get_file_storage();
60         check_dir_exists($CFG->tempdir.'/zip');
61         $tmpfile = tempnam($CFG->tempdir.'/zip', 'zipstor');
63         if ($result = $this->archive_to_pathname($files, $tmpfile, $ignoreinvalidfiles)) {
64             if ($file = $fs->get_file($contextid, $component, $filearea, $itemid, $filepath, $filename)) {
65                 if (!$file->delete()) {
66                     @unlink($tmpfile);
67                     return false;
68                 }
69             }
70             $file_record = new stdClass();
71             $file_record->contextid = $contextid;
72             $file_record->component = $component;
73             $file_record->filearea  = $filearea;
74             $file_record->itemid    = $itemid;
75             $file_record->filepath  = $filepath;
76             $file_record->filename  = $filename;
77             $file_record->userid    = $userid;
78             $file_record->mimetype  = 'application/zip';
80             $result = $fs->create_file_from_pathname($file_record, $tmpfile);
81         }
82         @unlink($tmpfile);
83         return $result;
84     }
86     /**
87      * Zip files and store the result in os file.
88      *
89      * @param array $files array with zip paths as keys (archivepath=>ospathname or archivepath=>stored_file or archivepath=>array('content_as_string'))
90      * @param string $archivefile path to target zip file
91      * @param bool $ignoreinvalidfiles true means ignore missing or invalid files, false means abort on any error
92      * @return bool true if file created, false if not
93      */
94     public function archive_to_pathname(array $files, $archivefile, $ignoreinvalidfiles=true) {
95         $ziparch = new zip_archive();
96         if (!$ziparch->open($archivefile, file_archive::OVERWRITE)) {
97             return false;
98         }
100         $abort = false;
101         foreach ($files as $archivepath => $file) {
102             $archivepath = trim($archivepath, '/');
104             if (is_null($file)) {
105                 // Directories have null as content.
106                 if (!$ziparch->add_directory($archivepath.'/')) {
107                     debugging("Can not zip '$archivepath' directory", DEBUG_DEVELOPER);
108                     if (!$ignoreinvalidfiles) {
109                         $abort = true;
110                         break;
111                     }
112                 }
114             } else if (is_string($file)) {
115                 if (!$this->archive_pathname($ziparch, $archivepath, $file)) {
116                     debugging("Can not zip '$archivepath' file", DEBUG_DEVELOPER);
117                     if (!$ignoreinvalidfiles) {
118                         $abort = true;
119                         break;
120                     }
121                 }
123             } else if (is_array($file)) {
124                 $content = reset($file);
125                 if (!$ziparch->add_file_from_string($archivepath, $content)) {
126                     debugging("Can not zip '$archivepath' file", DEBUG_DEVELOPER);
127                     if (!$ignoreinvalidfiles) {
128                         $abort = true;
129                         break;
130                     }
131                 }
133             } else {
134                 if (!$this->archive_stored($ziparch, $archivepath, $file)) {
135                     debugging("Can not zip '$archivepath' file", DEBUG_DEVELOPER);
136                     if (!$ignoreinvalidfiles) {
137                         $abort = true;
138                         break;
139                     }
140                 }
141             }
142         }
144         if (!$ziparch->close()) {
145             @unlink($archivefile);
146             return false;
147         }
149         if ($abort) {
150             @unlink($archivefile);
151             return false;
152         }
154         return true;
155     }
157     /**
158      * Perform archiving file from stored file.
159      *
160      * @param zip_archive $ziparch zip archive instance
161      * @param string $archivepath file path to archive
162      * @param stored_file $file stored_file object
163      * @return bool success
164      */
165     private function archive_stored($ziparch, $archivepath, $file) {
166         $result = $file->archive_file($ziparch, $archivepath);
167         if (!$result) {
168             return false;
169         }
171         if (!$file->is_directory()) {
172             return true;
173         }
175         $baselength = strlen($file->get_filepath());
176         $fs = get_file_storage();
177         $files = $fs->get_directory_files($file->get_contextid(), $file->get_component(), $file->get_filearea(), $file->get_itemid(),
178                                           $file->get_filepath(), true, true);
179         foreach ($files as $file) {
180             $path = $file->get_filepath();
181             $path = substr($path, $baselength);
182             $path = $archivepath.'/'.$path;
183             if (!$file->is_directory()) {
184                 $path = $path.$file->get_filename();
185             }
186             // Ignore result here, partial zipping is ok for now.
187             $file->archive_file($ziparch, $path);
188         }
190         return true;
191     }
193     /**
194      * Perform archiving file from file path.
195      *
196      * @param zip_archive $ziparch zip archive instance
197      * @param string $archivepath file path to archive
198      * @param string $file path name of the file
199      * @return bool success
200      */
201     private function archive_pathname($ziparch, $archivepath, $file) {
202         if (!file_exists($file)) {
203             return false;
204         }
206         if (is_file($file)) {
207             if (!is_readable($file)) {
208                 return false;
209             }
210             return $ziparch->add_file_from_pathname($archivepath, $file);
211         }
212         if (is_dir($file)) {
213             if ($archivepath !== '') {
214                 $ziparch->add_directory($archivepath);
215             }
216             $files = new DirectoryIterator($file);
217             foreach ($files as $file) {
218                 if ($file->isDot()) {
219                     continue;
220                 }
221                 $newpath = $archivepath.'/'.$file->getFilename();
222                 $this->archive_pathname($ziparch, $newpath, $file->getPathname());
223             }
224             unset($files); // Release file handles.
225             return true;
226         }
227     }
229     /**
230      * Unzip file to given file path (real OS filesystem), existing files are overwritten.
231      *
232      * @todo MDL-31048 localise messages
233      * @param string|stored_file $archivefile full pathname of zip file or stored_file instance
234      * @param string $pathname target directory
235      * @param array $onlyfiles only extract files present in the array. The path to files MUST NOT
236      *              start with a /. Example: array('myfile.txt', 'directory/anotherfile.txt')
237      * @return bool|array list of processed files; false if error
238      */
239     public function extract_to_pathname($archivefile, $pathname, array $onlyfiles = null) {
240         global $CFG;
242         if (!is_string($archivefile)) {
243             return $archivefile->extract_to_pathname($this, $pathname);
244         }
246         $processed = array();
248         $pathname = rtrim($pathname, '/');
249         if (!is_readable($archivefile)) {
250             return false;
251         }
252         $ziparch = new zip_archive();
253         if (!$ziparch->open($archivefile, file_archive::OPEN)) {
254             return false;
255         }
257         foreach ($ziparch as $info) {
258             $size = $info->size;
259             $name = $info->pathname;
261             if ($name === '' or array_key_exists($name, $processed)) {
262                 // Probably filename collisions caused by filename cleaning/conversion.
263                 continue;
264             } else if (is_array($onlyfiles) && !in_array($name, $onlyfiles)) {
265                 // Skipping files which are not in the list.
266                 continue;
267             }
269             if ($info->is_directory) {
270                 $newdir = "$pathname/$name";
271                 // directory
272                 if (is_file($newdir) and !unlink($newdir)) {
273                     $processed[$name] = 'Can not create directory, file already exists'; // TODO: localise
274                     continue;
275                 }
276                 if (is_dir($newdir)) {
277                     //dir already there
278                     $processed[$name] = true;
279                 } else {
280                     if (mkdir($newdir, $CFG->directorypermissions, true)) {
281                         $processed[$name] = true;
282                     } else {
283                         $processed[$name] = 'Can not create directory'; // TODO: localise
284                     }
285                 }
286                 continue;
287             }
289             $parts = explode('/', trim($name, '/'));
290             $filename = array_pop($parts);
291             $newdir = rtrim($pathname.'/'.implode('/', $parts), '/');
293             if (!is_dir($newdir)) {
294                 if (!mkdir($newdir, $CFG->directorypermissions, true)) {
295                     $processed[$name] = 'Can not create directory'; // TODO: localise
296                     continue;
297                 }
298             }
300             $newfile = "$newdir/$filename";
301             if (!$fp = fopen($newfile, 'wb')) {
302                 $processed[$name] = 'Can not write target file'; // TODO: localise
303                 continue;
304             }
305             if (!$fz = $ziparch->get_stream($info->index)) {
306                 $processed[$name] = 'Can not read file from zip archive'; // TODO: localise
307                 fclose($fp);
308                 continue;
309             }
311             while (!feof($fz)) {
312                 $content = fread($fz, 262143);
313                 fwrite($fp, $content);
314             }
315             fclose($fz);
316             fclose($fp);
317             if (filesize($newfile) !== $size) {
318                 $processed[$name] = 'Unknown error during zip extraction'; // TODO: localise
319                 // something went wrong :-(
320                 @unlink($newfile);
321                 continue;
322             }
323             $processed[$name] = true;
324         }
325         $ziparch->close();
326         return $processed;
327     }
329     /**
330      * Unzip file to given file path (real OS filesystem), existing files are overwritten.
331      *
332      * @todo MDL-31048 localise messages
333      * @param string|stored_file $archivefile full pathname of zip file or stored_file instance
334      * @param int $contextid context ID
335      * @param string $component component
336      * @param string $filearea file area
337      * @param int $itemid item ID
338      * @param string $pathbase file path
339      * @param int $userid user ID
340      * @return array|bool list of processed files; false if error
341      */
342     public function extract_to_storage($archivefile, $contextid, $component, $filearea, $itemid, $pathbase, $userid = NULL) {
343         global $CFG;
345         if (!is_string($archivefile)) {
346             return $archivefile->extract_to_storage($this, $contextid, $component, $filearea, $itemid, $pathbase, $userid);
347         }
349         check_dir_exists($CFG->tempdir.'/zip');
351         $pathbase = trim($pathbase, '/');
352         $pathbase = ($pathbase === '') ? '/' : '/'.$pathbase.'/';
353         $fs = get_file_storage();
355         $processed = array();
357         $ziparch = new zip_archive();
358         if (!$ziparch->open($archivefile, file_archive::OPEN)) {
359             return false;
360         }
362         foreach ($ziparch as $info) {
363             $size = $info->size;
364             $name = $info->pathname;
366             if ($name === '' or array_key_exists($name, $processed)) {
367                 //probably filename collisions caused by filename cleaning/conversion
368                 continue;
369             }
371             if ($info->is_directory) {
372                 $newfilepath = $pathbase.$name.'/';
373                 $fs->create_directory($contextid, $component, $filearea, $itemid, $newfilepath, $userid);
374                 $processed[$name] = true;
375                 continue;
376             }
378             $parts = explode('/', trim($name, '/'));
379             $filename = array_pop($parts);
380             $filepath = $pathbase;
381             if ($parts) {
382                 $filepath .= implode('/', $parts).'/';
383             }
385             if ($size < 2097151) {
386                 // Small file.
387                 if (!$fz = $ziparch->get_stream($info->index)) {
388                     $processed[$name] = 'Can not read file from zip archive'; // TODO: localise
389                     continue;
390                 }
391                 $content = '';
392                 while (!feof($fz)) {
393                     $content .= fread($fz, 262143);
394                 }
395                 fclose($fz);
396                 if (strlen($content) !== $size) {
397                     $processed[$name] = 'Unknown error during zip extraction'; // TODO: localise
398                     // something went wrong :-(
399                     unset($content);
400                     continue;
401                 }
403                 if ($file = $fs->get_file($contextid, $component, $filearea, $itemid, $filepath, $filename)) {
404                     if (!$file->delete()) {
405                         $processed[$name] = 'Can not delete existing file'; // TODO: localise
406                         continue;
407                     }
408                 }
409                 $file_record = new stdClass();
410                 $file_record->contextid = $contextid;
411                 $file_record->component = $component;
412                 $file_record->filearea  = $filearea;
413                 $file_record->itemid    = $itemid;
414                 $file_record->filepath  = $filepath;
415                 $file_record->filename  = $filename;
416                 $file_record->userid    = $userid;
417                 if ($fs->create_file_from_string($file_record, $content)) {
418                     $processed[$name] = true;
419                 } else {
420                     $processed[$name] = 'Unknown error during zip extraction'; // TODO: localise
421                 }
422                 unset($content);
423                 continue;
425             } else {
426                 // large file, would not fit into memory :-(
427                 $tmpfile = tempnam($CFG->tempdir.'/zip', 'unzip');
428                 if (!$fp = fopen($tmpfile, 'wb')) {
429                     @unlink($tmpfile);
430                     $processed[$name] = 'Can not write temp file'; // TODO: localise
431                     continue;
432                 }
433                 if (!$fz = $ziparch->get_stream($info->index)) {
434                     @unlink($tmpfile);
435                     $processed[$name] = 'Can not read file from zip archive'; // TODO: localise
436                     continue;
437                 }
438                 while (!feof($fz)) {
439                     $content = fread($fz, 262143);
440                     fwrite($fp, $content);
441                 }
442                 fclose($fz);
443                 fclose($fp);
444                 if (filesize($tmpfile) !== $size) {
445                     $processed[$name] = 'Unknown error during zip extraction'; // TODO: localise
446                     // something went wrong :-(
447                     @unlink($tmpfile);
448                     continue;
449                 }
451                 if ($file = $fs->get_file($contextid, $component, $filearea, $itemid, $filepath, $filename)) {
452                     if (!$file->delete()) {
453                         @unlink($tmpfile);
454                         $processed[$name] = 'Can not delete existing file'; // TODO: localise
455                         continue;
456                     }
457                 }
458                 $file_record = new stdClass();
459                 $file_record->contextid = $contextid;
460                 $file_record->component = $component;
461                 $file_record->filearea  = $filearea;
462                 $file_record->itemid    = $itemid;
463                 $file_record->filepath  = $filepath;
464                 $file_record->filename  = $filename;
465                 $file_record->userid    = $userid;
466                 if ($fs->create_file_from_pathname($file_record, $tmpfile)) {
467                     $processed[$name] = true;
468                 } else {
469                     $processed[$name] = 'Unknown error during zip extraction'; // TODO: localise
470                 }
471                 @unlink($tmpfile);
472                 continue;
473             }
474         }
475         $ziparch->close();
476         return $processed;
477     }
479     /**
480      * Returns array of info about all files in archive.
481      *
482      * @param string|file_archive $archivefile
483      * @return array of file infos
484      */
485     public function list_files($archivefile) {
486         if (!is_string($archivefile)) {
487             return $archivefile->list_files();
488         }
490         $ziparch = new zip_archive();
491         if (!$ziparch->open($archivefile, file_archive::OPEN)) {
492             return false;
493         }
494         $list = $ziparch->list_files();
495         $ziparch->close();
496         return $list;
497     }