MDL-41839 Files: Zip packer progress minor bugs
[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      * @param file_progress $progress Progress indicator callback or null if not required
54      * @return stored_file|bool false if error stored_file instance if ok
55      */
56     public function archive_to_storage(array $files, $contextid,
57             $component, $filearea, $itemid, $filepath, $filename,
58             $userid = NULL, $ignoreinvalidfiles=true, file_progress $progress = null) {
59         global $CFG;
61         $fs = get_file_storage();
63         check_dir_exists($CFG->tempdir.'/zip');
64         $tmpfile = tempnam($CFG->tempdir.'/zip', 'zipstor');
66         if ($result = $this->archive_to_pathname($files, $tmpfile, $ignoreinvalidfiles, $progress)) {
67             if ($file = $fs->get_file($contextid, $component, $filearea, $itemid, $filepath, $filename)) {
68                 if (!$file->delete()) {
69                     @unlink($tmpfile);
70                     return false;
71                 }
72             }
73             $file_record = new stdClass();
74             $file_record->contextid = $contextid;
75             $file_record->component = $component;
76             $file_record->filearea  = $filearea;
77             $file_record->itemid    = $itemid;
78             $file_record->filepath  = $filepath;
79             $file_record->filename  = $filename;
80             $file_record->userid    = $userid;
81             $file_record->mimetype  = 'application/zip';
83             $result = $fs->create_file_from_pathname($file_record, $tmpfile);
84         }
85         @unlink($tmpfile);
86         return $result;
87     }
89     /**
90      * Zip files and store the result in os file.
91      *
92      * @param array $files array with zip paths as keys (archivepath=>ospathname or archivepath=>stored_file or archivepath=>array('content_as_string'))
93      * @param string $archivefile path to target zip file
94      * @param bool $ignoreinvalidfiles true means ignore missing or invalid files, false means abort on any error
95      * @param file_progress $progress Progress indicator callback or null if not required
96      * @return bool true if file created, false if not
97      */
98     public function archive_to_pathname(array $files, $archivefile,
99             $ignoreinvalidfiles=true, file_progress $progress = null) {
100         $ziparch = new zip_archive();
101         if (!$ziparch->open($archivefile, file_archive::OVERWRITE)) {
102             return false;
103         }
105         $abort = false;
106         foreach ($files as $archivepath => $file) {
107             $archivepath = trim($archivepath, '/');
109             // Record progress each time around this loop.
110             if ($progress) {
111                 $progress->progress();
112             }
114             if (is_null($file)) {
115                 // Directories have null as content.
116                 if (!$ziparch->add_directory($archivepath.'/')) {
117                     debugging("Can not zip '$archivepath' directory", DEBUG_DEVELOPER);
118                     if (!$ignoreinvalidfiles) {
119                         $abort = true;
120                         break;
121                     }
122                 }
124             } else if (is_string($file)) {
125                 if (!$this->archive_pathname($ziparch, $archivepath, $file, $progress)) {
126                     debugging("Can not zip '$archivepath' file", DEBUG_DEVELOPER);
127                     if (!$ignoreinvalidfiles) {
128                         $abort = true;
129                         break;
130                     }
131                 }
133             } else if (is_array($file)) {
134                 $content = reset($file);
135                 if (!$ziparch->add_file_from_string($archivepath, $content)) {
136                     debugging("Can not zip '$archivepath' file", DEBUG_DEVELOPER);
137                     if (!$ignoreinvalidfiles) {
138                         $abort = true;
139                         break;
140                     }
141                 }
143             } else {
144                 if (!$this->archive_stored($ziparch, $archivepath, $file, $progress)) {
145                     debugging("Can not zip '$archivepath' file", DEBUG_DEVELOPER);
146                     if (!$ignoreinvalidfiles) {
147                         $abort = true;
148                         break;
149                     }
150                 }
151             }
152         }
154         if (!$ziparch->close()) {
155             @unlink($archivefile);
156             return false;
157         }
159         if ($abort) {
160             @unlink($archivefile);
161             return false;
162         }
164         return true;
165     }
167     /**
168      * Perform archiving file from stored file.
169      *
170      * @param zip_archive $ziparch zip archive instance
171      * @param string $archivepath file path to archive
172      * @param stored_file $file stored_file object
173      * @param file_progress $progress Progress indicator callback or null if not required
174      * @return bool success
175      */
176     private function archive_stored($ziparch, $archivepath, $file, file_progress $progress = null) {
177         $result = $file->archive_file($ziparch, $archivepath);
178         if (!$result) {
179             return false;
180         }
182         if (!$file->is_directory()) {
183             return true;
184         }
186         $baselength = strlen($file->get_filepath());
187         $fs = get_file_storage();
188         $files = $fs->get_directory_files($file->get_contextid(), $file->get_component(), $file->get_filearea(), $file->get_itemid(),
189                                           $file->get_filepath(), true, true);
190         foreach ($files as $file) {
191             // Record progress for each file.
192             if ($progress) {
193                 $progress->progress();
194             }
196             $path = $file->get_filepath();
197             $path = substr($path, $baselength);
198             $path = $archivepath.'/'.$path;
199             if (!$file->is_directory()) {
200                 $path = $path.$file->get_filename();
201             }
202             // Ignore result here, partial zipping is ok for now.
203             $file->archive_file($ziparch, $path);
204         }
206         return true;
207     }
209     /**
210      * Perform archiving file from file path.
211      *
212      * @param zip_archive $ziparch zip archive instance
213      * @param string $archivepath file path to archive
214      * @param string $file path name of the file
215      * @param file_progress $progress Progress indicator callback or null if not required
216      * @return bool success
217      */
218     private function archive_pathname($ziparch, $archivepath, $file,
219             file_progress $progress = null) {
220         // Record progress each time this function is called.
221         if ($progress) {
222             $progress->progress();
223         }
225         if (!file_exists($file)) {
226             return false;
227         }
229         if (is_file($file)) {
230             if (!is_readable($file)) {
231                 return false;
232             }
233             return $ziparch->add_file_from_pathname($archivepath, $file);
234         }
235         if (is_dir($file)) {
236             if ($archivepath !== '') {
237                 $ziparch->add_directory($archivepath);
238             }
239             $files = new DirectoryIterator($file);
240             foreach ($files as $file) {
241                 if ($file->isDot()) {
242                     continue;
243                 }
244                 $newpath = $archivepath.'/'.$file->getFilename();
245                 $this->archive_pathname($ziparch, $newpath, $file->getPathname(), $progress);
246             }
247             unset($files); // Release file handles.
248             return true;
249         }
250     }
252     /**
253      * Unzip file to given file path (real OS filesystem), existing files are overwritten.
254      *
255      * @todo MDL-31048 localise messages
256      * @param string|stored_file $archivefile full pathname of zip file or stored_file instance
257      * @param string $pathname target directory
258      * @param array $onlyfiles only extract files present in the array. The path to files MUST NOT
259      *              start with a /. Example: array('myfile.txt', 'directory/anotherfile.txt')
260      * @param file_progress $progress Progress indicator callback or null if not required
261      * @return bool|array list of processed files; false if error
262      */
263     public function extract_to_pathname($archivefile, $pathname,
264             array $onlyfiles = null, file_progress $progress = null) {
265         global $CFG;
267         if (!is_string($archivefile)) {
268             return $archivefile->extract_to_pathname($this, $pathname, $progress);
269         }
271         $processed = array();
273         $pathname = rtrim($pathname, '/');
274         if (!is_readable($archivefile)) {
275             return false;
276         }
277         $ziparch = new zip_archive();
278         if (!$ziparch->open($archivefile, file_archive::OPEN)) {
279             return false;
280         }
282         // Get the number of files (approx).
283         if ($progress) {
284             $approxmax = $ziparch->estimated_count();
285             $done = 0;
286         }
288         foreach ($ziparch as $info) {
289             // Notify progress.
290             if ($progress) {
291                 $progress->progress($done, $approxmax);
292                 $done++;
293             }
295             $size = $info->size;
296             $name = $info->pathname;
298             if ($name === '' or array_key_exists($name, $processed)) {
299                 // Probably filename collisions caused by filename cleaning/conversion.
300                 continue;
301             } else if (is_array($onlyfiles) && !in_array($name, $onlyfiles)) {
302                 // Skipping files which are not in the list.
303                 continue;
304             }
306             if ($info->is_directory) {
307                 $newdir = "$pathname/$name";
308                 // directory
309                 if (is_file($newdir) and !unlink($newdir)) {
310                     $processed[$name] = 'Can not create directory, file already exists'; // TODO: localise
311                     continue;
312                 }
313                 if (is_dir($newdir)) {
314                     //dir already there
315                     $processed[$name] = true;
316                 } else {
317                     if (mkdir($newdir, $CFG->directorypermissions, true)) {
318                         $processed[$name] = true;
319                     } else {
320                         $processed[$name] = 'Can not create directory'; // TODO: localise
321                     }
322                 }
323                 continue;
324             }
326             $parts = explode('/', trim($name, '/'));
327             $filename = array_pop($parts);
328             $newdir = rtrim($pathname.'/'.implode('/', $parts), '/');
330             if (!is_dir($newdir)) {
331                 if (!mkdir($newdir, $CFG->directorypermissions, true)) {
332                     $processed[$name] = 'Can not create directory'; // TODO: localise
333                     continue;
334                 }
335             }
337             $newfile = "$newdir/$filename";
338             if (!$fp = fopen($newfile, 'wb')) {
339                 $processed[$name] = 'Can not write target file'; // TODO: localise
340                 continue;
341             }
342             if (!$fz = $ziparch->get_stream($info->index)) {
343                 $processed[$name] = 'Can not read file from zip archive'; // TODO: localise
344                 fclose($fp);
345                 continue;
346             }
348             while (!feof($fz)) {
349                 $content = fread($fz, 262143);
350                 fwrite($fp, $content);
351             }
352             fclose($fz);
353             fclose($fp);
354             if (filesize($newfile) !== $size) {
355                 $processed[$name] = 'Unknown error during zip extraction'; // TODO: localise
356                 // something went wrong :-(
357                 @unlink($newfile);
358                 continue;
359             }
360             $processed[$name] = true;
361         }
362         $ziparch->close();
363         return $processed;
364     }
366     /**
367      * Unzip file to given file path (real OS filesystem), existing files are overwritten.
368      *
369      * @todo MDL-31048 localise messages
370      * @param string|stored_file $archivefile full pathname of zip file or stored_file instance
371      * @param int $contextid context ID
372      * @param string $component component
373      * @param string $filearea file area
374      * @param int $itemid item ID
375      * @param string $pathbase file path
376      * @param int $userid user ID
377      * @param file_progress $progress Progress indicator callback or null if not required
378      * @return array|bool list of processed files; false if error
379      */
380     public function extract_to_storage($archivefile, $contextid,
381             $component, $filearea, $itemid, $pathbase, $userid = NULL,
382             file_progress $progress = null) {
383         global $CFG;
385         if (!is_string($archivefile)) {
386             return $archivefile->extract_to_storage($this, $contextid, $component,
387                     $filearea, $itemid, $pathbase, $userid, $progress);
388         }
390         check_dir_exists($CFG->tempdir.'/zip');
392         $pathbase = trim($pathbase, '/');
393         $pathbase = ($pathbase === '') ? '/' : '/'.$pathbase.'/';
394         $fs = get_file_storage();
396         $processed = array();
398         $ziparch = new zip_archive();
399         if (!$ziparch->open($archivefile, file_archive::OPEN)) {
400             return false;
401         }
403         // Get the number of files (approx).
404         if ($progress) {
405             $approxmax = $ziparch->estimated_count();
406             $done = 0;
407         }
409         foreach ($ziparch as $info) {
410             // Notify progress.
411             if ($progress) {
412                 $progress->progress($done, $approxmax);
413                 $done++;
414             }
416             $size = $info->size;
417             $name = $info->pathname;
419             if ($name === '' or array_key_exists($name, $processed)) {
420                 //probably filename collisions caused by filename cleaning/conversion
421                 continue;
422             }
424             if ($info->is_directory) {
425                 $newfilepath = $pathbase.$name.'/';
426                 $fs->create_directory($contextid, $component, $filearea, $itemid, $newfilepath, $userid);
427                 $processed[$name] = true;
428                 continue;
429             }
431             $parts = explode('/', trim($name, '/'));
432             $filename = array_pop($parts);
433             $filepath = $pathbase;
434             if ($parts) {
435                 $filepath .= implode('/', $parts).'/';
436             }
438             if ($size < 2097151) {
439                 // Small file.
440                 if (!$fz = $ziparch->get_stream($info->index)) {
441                     $processed[$name] = 'Can not read file from zip archive'; // TODO: localise
442                     continue;
443                 }
444                 $content = '';
445                 while (!feof($fz)) {
446                     $content .= fread($fz, 262143);
447                 }
448                 fclose($fz);
449                 if (strlen($content) !== $size) {
450                     $processed[$name] = 'Unknown error during zip extraction'; // TODO: localise
451                     // something went wrong :-(
452                     unset($content);
453                     continue;
454                 }
456                 if ($file = $fs->get_file($contextid, $component, $filearea, $itemid, $filepath, $filename)) {
457                     if (!$file->delete()) {
458                         $processed[$name] = 'Can not delete existing file'; // TODO: localise
459                         continue;
460                     }
461                 }
462                 $file_record = new stdClass();
463                 $file_record->contextid = $contextid;
464                 $file_record->component = $component;
465                 $file_record->filearea  = $filearea;
466                 $file_record->itemid    = $itemid;
467                 $file_record->filepath  = $filepath;
468                 $file_record->filename  = $filename;
469                 $file_record->userid    = $userid;
470                 if ($fs->create_file_from_string($file_record, $content)) {
471                     $processed[$name] = true;
472                 } else {
473                     $processed[$name] = 'Unknown error during zip extraction'; // TODO: localise
474                 }
475                 unset($content);
476                 continue;
478             } else {
479                 // large file, would not fit into memory :-(
480                 $tmpfile = tempnam($CFG->tempdir.'/zip', 'unzip');
481                 if (!$fp = fopen($tmpfile, 'wb')) {
482                     @unlink($tmpfile);
483                     $processed[$name] = 'Can not write temp file'; // TODO: localise
484                     continue;
485                 }
486                 if (!$fz = $ziparch->get_stream($info->index)) {
487                     @unlink($tmpfile);
488                     $processed[$name] = 'Can not read file from zip archive'; // TODO: localise
489                     continue;
490                 }
491                 while (!feof($fz)) {
492                     $content = fread($fz, 262143);
493                     fwrite($fp, $content);
494                 }
495                 fclose($fz);
496                 fclose($fp);
497                 if (filesize($tmpfile) !== $size) {
498                     $processed[$name] = 'Unknown error during zip extraction'; // TODO: localise
499                     // something went wrong :-(
500                     @unlink($tmpfile);
501                     continue;
502                 }
504                 if ($file = $fs->get_file($contextid, $component, $filearea, $itemid, $filepath, $filename)) {
505                     if (!$file->delete()) {
506                         @unlink($tmpfile);
507                         $processed[$name] = 'Can not delete existing file'; // TODO: localise
508                         continue;
509                     }
510                 }
511                 $file_record = new stdClass();
512                 $file_record->contextid = $contextid;
513                 $file_record->component = $component;
514                 $file_record->filearea  = $filearea;
515                 $file_record->itemid    = $itemid;
516                 $file_record->filepath  = $filepath;
517                 $file_record->filename  = $filename;
518                 $file_record->userid    = $userid;
519                 if ($fs->create_file_from_pathname($file_record, $tmpfile)) {
520                     $processed[$name] = true;
521                 } else {
522                     $processed[$name] = 'Unknown error during zip extraction'; // TODO: localise
523                 }
524                 @unlink($tmpfile);
525                 continue;
526             }
527         }
528         $ziparch->close();
529         return $processed;
530     }
532     /**
533      * Returns array of info about all files in archive.
534      *
535      * @param string|file_archive $archivefile
536      * @return array of file infos
537      */
538     public function list_files($archivefile) {
539         if (!is_string($archivefile)) {
540             return $archivefile->list_files();
541         }
543         $ziparch = new zip_archive();
544         if (!$ziparch->open($archivefile, file_archive::OPEN)) {
545             return false;
546         }
547         $list = $ziparch->list_files();
548         $ziparch->close();
549         return $list;
550     }