Merge branch 'MDL-64012' of https://github.com/timhunt/moodle
[moodle.git] / lib / filestorage / zip_packer.php
blob47395f73b2482a20e4ad7f3c4ce2e84cd47dc95b
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.
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.
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
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.
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
38 class zip_packer extends file_packer {
40 /**
41 * Zip files and store the result in file storage.
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
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;
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);
85 @unlink($tmpfile);
86 return $result;
89 /**
90 * Zip files and store the result in os file.
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
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;
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();
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;
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;
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;
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;
154 if (!$ziparch->close()) {
155 @unlink($archivefile);
156 return false;
159 if ($abort) {
160 @unlink($archivefile);
161 return false;
164 return true;
168 * Perform archiving file from stored file.
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
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;
182 if (!$file->is_directory()) {
183 return true;
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();
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();
202 // Ignore result here, partial zipping is ok for now.
203 $file->archive_file($ziparch, $path);
206 return true;
210 * Perform archiving file from file path.
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
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();
225 if (!file_exists($file)) {
226 return false;
229 if (is_file($file)) {
230 if (!is_readable($file)) {
231 return false;
233 return $ziparch->add_file_from_pathname($archivepath, $file);
235 if (is_dir($file)) {
236 if ($archivepath !== '') {
237 $ziparch->add_directory($archivepath);
239 $files = new DirectoryIterator($file);
240 foreach ($files as $file) {
241 if ($file->isDot()) {
242 continue;
244 $newpath = $archivepath.'/'.$file->getFilename();
245 $this->archive_pathname($ziparch, $newpath, $file->getPathname(), $progress);
247 unset($files); // Release file handles.
248 return true;
253 * Unzip file to given file path (real OS filesystem), existing files are overwritten.
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 * @param bool $returnbool Whether to return a basic true/false indicating error state, or full per-file error
262 * details.
263 * @return bool|array list of processed files; false if error
265 public function extract_to_pathname($archivefile, $pathname,
266 array $onlyfiles = null, file_progress $progress = null, $returnbool = false) {
267 global $CFG;
269 if (!is_string($archivefile)) {
270 return $archivefile->extract_to_pathname($this, $pathname, $progress);
273 $processed = array();
274 $success = true;
276 $pathname = rtrim($pathname, '/');
277 if (!is_readable($archivefile)) {
278 return false;
280 $ziparch = new zip_archive();
281 if (!$ziparch->open($archivefile, file_archive::OPEN)) {
282 return false;
285 // Get the number of files (approx).
286 if ($progress) {
287 $approxmax = $ziparch->estimated_count();
288 $done = 0;
291 foreach ($ziparch as $info) {
292 // Notify progress.
293 if ($progress) {
294 $progress->progress($done, $approxmax);
295 $done++;
298 $size = $info->size;
299 $name = $info->pathname;
301 if ($name === '' or array_key_exists($name, $processed)) {
302 // Probably filename collisions caused by filename cleaning/conversion.
303 continue;
304 } else if (is_array($onlyfiles) && !in_array($name, $onlyfiles)) {
305 // Skipping files which are not in the list.
306 continue;
309 if ($info->is_directory) {
310 $newdir = "$pathname/$name";
311 // directory
312 if (is_file($newdir) and !unlink($newdir)) {
313 $processed[$name] = 'Can not create directory, file already exists'; // TODO: localise
314 $success = false;
315 continue;
317 if (is_dir($newdir)) {
318 //dir already there
319 $processed[$name] = true;
320 } else {
321 if (mkdir($newdir, $CFG->directorypermissions, true)) {
322 $processed[$name] = true;
323 } else {
324 $processed[$name] = 'Can not create directory'; // TODO: localise
325 $success = false;
328 continue;
331 $parts = explode('/', trim($name, '/'));
332 $filename = array_pop($parts);
333 $newdir = rtrim($pathname.'/'.implode('/', $parts), '/');
335 if (!is_dir($newdir)) {
336 if (!mkdir($newdir, $CFG->directorypermissions, true)) {
337 $processed[$name] = 'Can not create directory'; // TODO: localise
338 $success = false;
339 continue;
343 $newfile = "$newdir/$filename";
344 if (!$fp = fopen($newfile, 'wb')) {
345 $processed[$name] = 'Can not write target file'; // TODO: localise
346 $success = false;
347 continue;
349 if (!$fz = $ziparch->get_stream($info->index)) {
350 $processed[$name] = 'Can not read file from zip archive'; // TODO: localise
351 $success = false;
352 fclose($fp);
353 continue;
356 while (!feof($fz)) {
357 $content = fread($fz, 262143);
358 fwrite($fp, $content);
360 fclose($fz);
361 fclose($fp);
362 if (filesize($newfile) !== $size) {
363 $processed[$name] = 'Unknown error during zip extraction'; // TODO: localise
364 $success = false;
365 // something went wrong :-(
366 @unlink($newfile);
367 continue;
369 $processed[$name] = true;
371 $ziparch->close();
373 if ($returnbool) {
374 return $success;
375 } else {
376 return $processed;
381 * Unzip file to given file path (real OS filesystem), existing files are overwritten.
383 * @todo MDL-31048 localise messages
384 * @param string|stored_file $archivefile full pathname of zip file or stored_file instance
385 * @param int $contextid context ID
386 * @param string $component component
387 * @param string $filearea file area
388 * @param int $itemid item ID
389 * @param string $pathbase file path
390 * @param int $userid user ID
391 * @param file_progress $progress Progress indicator callback or null if not required
392 * @return array|bool list of processed files; false if error
394 public function extract_to_storage($archivefile, $contextid,
395 $component, $filearea, $itemid, $pathbase, $userid = NULL,
396 file_progress $progress = null) {
397 global $CFG;
399 if (!is_string($archivefile)) {
400 return $archivefile->extract_to_storage($this, $contextid, $component,
401 $filearea, $itemid, $pathbase, $userid, $progress);
404 check_dir_exists($CFG->tempdir.'/zip');
406 $pathbase = trim($pathbase, '/');
407 $pathbase = ($pathbase === '') ? '/' : '/'.$pathbase.'/';
408 $fs = get_file_storage();
410 $processed = array();
412 $ziparch = new zip_archive();
413 if (!$ziparch->open($archivefile, file_archive::OPEN)) {
414 return false;
417 // Get the number of files (approx).
418 if ($progress) {
419 $approxmax = $ziparch->estimated_count();
420 $done = 0;
423 foreach ($ziparch as $info) {
424 // Notify progress.
425 if ($progress) {
426 $progress->progress($done, $approxmax);
427 $done++;
430 $size = $info->size;
431 $name = $info->pathname;
433 if ($name === '' or array_key_exists($name, $processed)) {
434 //probably filename collisions caused by filename cleaning/conversion
435 continue;
438 if ($info->is_directory) {
439 $newfilepath = $pathbase.$name.'/';
440 $fs->create_directory($contextid, $component, $filearea, $itemid, $newfilepath, $userid);
441 $processed[$name] = true;
442 continue;
445 $parts = explode('/', trim($name, '/'));
446 $filename = array_pop($parts);
447 $filepath = $pathbase;
448 if ($parts) {
449 $filepath .= implode('/', $parts).'/';
452 if ($size < 2097151) {
453 // Small file.
454 if (!$fz = $ziparch->get_stream($info->index)) {
455 $processed[$name] = 'Can not read file from zip archive'; // TODO: localise
456 continue;
458 $content = '';
459 while (!feof($fz)) {
460 $content .= fread($fz, 262143);
462 fclose($fz);
463 if (strlen($content) !== $size) {
464 $processed[$name] = 'Unknown error during zip extraction'; // TODO: localise
465 // something went wrong :-(
466 unset($content);
467 continue;
470 if ($file = $fs->get_file($contextid, $component, $filearea, $itemid, $filepath, $filename)) {
471 if (!$file->delete()) {
472 $processed[$name] = 'Can not delete existing file'; // TODO: localise
473 continue;
476 $file_record = new stdClass();
477 $file_record->contextid = $contextid;
478 $file_record->component = $component;
479 $file_record->filearea = $filearea;
480 $file_record->itemid = $itemid;
481 $file_record->filepath = $filepath;
482 $file_record->filename = $filename;
483 $file_record->userid = $userid;
484 if ($fs->create_file_from_string($file_record, $content)) {
485 $processed[$name] = true;
486 } else {
487 $processed[$name] = 'Unknown error during zip extraction'; // TODO: localise
489 unset($content);
490 continue;
492 } else {
493 // large file, would not fit into memory :-(
494 $tmpfile = tempnam($CFG->tempdir.'/zip', 'unzip');
495 if (!$fp = fopen($tmpfile, 'wb')) {
496 @unlink($tmpfile);
497 $processed[$name] = 'Can not write temp file'; // TODO: localise
498 continue;
500 if (!$fz = $ziparch->get_stream($info->index)) {
501 @unlink($tmpfile);
502 $processed[$name] = 'Can not read file from zip archive'; // TODO: localise
503 continue;
505 while (!feof($fz)) {
506 $content = fread($fz, 262143);
507 fwrite($fp, $content);
509 fclose($fz);
510 fclose($fp);
511 if (filesize($tmpfile) !== $size) {
512 $processed[$name] = 'Unknown error during zip extraction'; // TODO: localise
513 // something went wrong :-(
514 @unlink($tmpfile);
515 continue;
518 if ($file = $fs->get_file($contextid, $component, $filearea, $itemid, $filepath, $filename)) {
519 if (!$file->delete()) {
520 @unlink($tmpfile);
521 $processed[$name] = 'Can not delete existing file'; // TODO: localise
522 continue;
525 $file_record = new stdClass();
526 $file_record->contextid = $contextid;
527 $file_record->component = $component;
528 $file_record->filearea = $filearea;
529 $file_record->itemid = $itemid;
530 $file_record->filepath = $filepath;
531 $file_record->filename = $filename;
532 $file_record->userid = $userid;
533 if ($fs->create_file_from_pathname($file_record, $tmpfile)) {
534 $processed[$name] = true;
535 } else {
536 $processed[$name] = 'Unknown error during zip extraction'; // TODO: localise
538 @unlink($tmpfile);
539 continue;
542 $ziparch->close();
543 return $processed;
547 * Returns array of info about all files in archive.
549 * @param string|file_archive $archivefile
550 * @return array of file infos
552 public function list_files($archivefile) {
553 if (!is_string($archivefile)) {
554 return $archivefile->list_files();
557 $ziparch = new zip_archive();
558 if (!$ziparch->open($archivefile, file_archive::OPEN)) {
559 return false;
561 $list = $ziparch->list_files();
562 $ziparch->close();
563 return $list;