MDL-37429 zipping improvements
[moodle.git] / lib / filestorage / zip_packer.php
CommitLineData
33488ad6 1<?php
33488ad6 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/>.
16
33488ad6 17/**
18 * Implementation of zip packer.
19 *
d2b7803e
DC
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
33488ad6 23 */
172dd12c 24
64f93798
PS
25defined('MOODLE_INTERNAL') || die();
26
27require_once("$CFG->libdir/filestorage/file_packer.php");
28require_once("$CFG->libdir/filestorage/zip_archive.php");
0b0bfa93 29
17d9269f 30/**
31 * Utility class - handles all zipping and unzipping operations.
64f93798 32 *
d2b7803e
DC
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
17d9269f 37 */
0b0bfa93 38class zip_packer extends file_packer {
172dd12c 39
b1897a6d 40 /**
01b4040a 41 * Zip files and store the result in file storage.
d2b7803e 42 *
59333bc9
PS
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'))
d2b7803e
DC
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
01b4040a
PS
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
b1897a6d 54 */
01b4040a 55 public function archive_to_storage(array $files, $contextid, $component, $filearea, $itemid, $filepath, $filename, $userid = NULL, $ignoreinvalidfiles=true) {
b1897a6d 56 global $CFG;
57
58 $fs = get_file_storage();
59
7aa06e6d
TL
60 check_dir_exists($CFG->tempdir.'/zip');
61 $tmpfile = tempnam($CFG->tempdir.'/zip', 'zipstor');
b1897a6d 62
01b4040a 63 if ($result = $this->archive_to_pathname($files, $tmpfile, $ignoreinvalidfiles)) {
64f93798 64 if ($file = $fs->get_file($contextid, $component, $filearea, $itemid, $filepath, $filename)) {
b1897a6d 65 if (!$file->delete()) {
66 @unlink($tmpfile);
67 return false;
68 }
69 }
ac6f1a82 70 $file_record = new stdClass();
b1897a6d 71 $file_record->contextid = $contextid;
64f93798 72 $file_record->component = $component;
b1897a6d 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;
b27c4855 78 $file_record->mimetype = 'application/zip';
0b0bfa93 79
b1897a6d 80 $result = $fs->create_file_from_pathname($file_record, $tmpfile);
81 }
82 @unlink($tmpfile);
83 return $result;
84 }
85
86 /**
01b4040a 87 * Zip files and store the result in os file.
d2b7803e 88 *
59333bc9 89 * @param array $files array with zip paths as keys (archivepath=>ospathname or archivepath=>stored_file or archivepath=>array('content_as_string'))
593bb2eb 90 * @param string $archivefile path to target zip file
01b4040a
PS
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
b1897a6d 93 */
01b4040a 94 public function archive_to_pathname(array $files, $archivefile, $ignoreinvalidfiles=true) {
b1897a6d 95 $ziparch = new zip_archive();
0b0bfa93 96 if (!$ziparch->open($archivefile, file_archive::OVERWRITE)) {
b1897a6d 97 return false;
98 }
99
01b4040a 100 $abort = false;
b1897a6d 101 foreach ($files as $archivepath => $file) {
102 $archivepath = trim($archivepath, '/');
103
104 if (is_null($file)) {
01b4040a
PS
105 // Directories have null as content.
106 if (!$ziparch->add_directory($archivepath.'/')) {
f6b49abf 107 debugging("Can not zip '$archivepath' directory", DEBUG_DEVELOPER);
01b4040a
PS
108 if (!$ignoreinvalidfiles) {
109 $abort = true;
110 break;
111 }
f6b49abf 112 }
b1897a6d 113
114 } else if (is_string($file)) {
01b4040a 115 if (!$this->archive_pathname($ziparch, $archivepath, $file)) {
f6b49abf 116 debugging("Can not zip '$archivepath' file", DEBUG_DEVELOPER);
01b4040a
PS
117 if (!$ignoreinvalidfiles) {
118 $abort = true;
119 break;
120 }
f6b49abf 121 }
b1897a6d 122
59333bc9
PS
123 } else if (is_array($file)) {
124 $content = reset($file);
01b4040a 125 if (!$ziparch->add_file_from_string($archivepath, $content)) {
f6b49abf 126 debugging("Can not zip '$archivepath' file", DEBUG_DEVELOPER);
01b4040a
PS
127 if (!$ignoreinvalidfiles) {
128 $abort = true;
129 break;
130 }
f6b49abf 131 }
59333bc9 132
b1897a6d 133 } else {
01b4040a 134 if (!$this->archive_stored($ziparch, $archivepath, $file)) {
f6b49abf 135 debugging("Can not zip '$archivepath' file", DEBUG_DEVELOPER);
01b4040a
PS
136 if (!$ignoreinvalidfiles) {
137 $abort = true;
138 break;
139 }
f6b49abf 140 }
b1897a6d 141 }
142 }
143
01b4040a
PS
144 if (!$ziparch->close()) {
145 @unlink($archivefile);
146 return false;
638d72cd
FM
147 }
148
01b4040a
PS
149 if ($abort) {
150 @unlink($archivefile);
151 return false;
152 }
153
154 return true;
17d9269f 155 }
156
d2b7803e 157 /**
01b4040a 158 * Perform archiving file from stored file.
d2b7803e
DC
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
f6b49abf 163 * @return bool success
d2b7803e 164 */
0b0bfa93 165 private function archive_stored($ziparch, $archivepath, $file) {
f6b49abf
PS
166 $result = $file->archive_file($ziparch, $archivepath);
167 if (!$result) {
168 return false;
169 }
b1897a6d 170
171 if (!$file->is_directory()) {
f6b49abf 172 return true;
b1897a6d 173 }
174
175 $baselength = strlen($file->get_filepath());
176 $fs = get_file_storage();
64f93798 177 $files = $fs->get_directory_files($file->get_contextid(), $file->get_component(), $file->get_filearea(), $file->get_itemid(),
b1897a6d 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 }
f6b49abf 186 // Ignore result here, partial zipping is ok for now.
0b0bfa93 187 $file->archive_file($ziparch, $path);
b1897a6d 188 }
f6b49abf
PS
189
190 return true;
b1897a6d 191 }
192
d2b7803e 193 /**
01b4040a 194 * Perform archiving file from file path.
d2b7803e
DC
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
f6b49abf 199 * @return bool success
d2b7803e 200 */
0b0bfa93 201 private function archive_pathname($ziparch, $archivepath, $file) {
b1897a6d 202 if (!file_exists($file)) {
f6b49abf 203 return false;
b1897a6d 204 }
205
206 if (is_file($file)) {
207 if (!is_readable($file)) {
f6b49abf 208 return false;
b1897a6d 209 }
f6b49abf 210 return $ziparch->add_file_from_pathname($archivepath, $file);
b1897a6d 211 }
212 if (is_dir($file)) {
213 if ($archivepath !== '') {
0b0bfa93 214 $ziparch->add_directory($archivepath);
b1897a6d 215 }
216 $files = new DirectoryIterator($file);
217 foreach ($files as $file) {
218 if ($file->isDot()) {
219 continue;
220 }
a14e283a 221 $newpath = $archivepath.'/'.$file->getFilename();
0b0bfa93 222 $this->archive_pathname($ziparch, $newpath, $file->getPathname());
b1897a6d 223 }
01b4040a 224 unset($files); // Release file handles.
f6b49abf 225 return true;
b1897a6d 226 }
17d9269f 227 }
228
229 /**
01b4040a 230 * Unzip file to given file path (real OS filesystem), existing files are overwritten.
d2b7803e
DC
231 *
232 * @todo MDL-31048 localise messages
233 * @param string|stored_file $archivefile full pathname of zip file or stored_file instance
b1897a6d 234 * @param string $pathname target directory
e462f46b
FM
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')
d2b7803e 237 * @return bool|array list of processed files; false if error
17d9269f 238 */
e462f46b 239 public function extract_to_pathname($archivefile, $pathname, array $onlyfiles = null) {
17d9269f 240 global $CFG;
241
0b0bfa93 242 if (!is_string($archivefile)) {
243 return $archivefile->extract_to_pathname($this, $pathname);
17d9269f 244 }
245
246 $processed = array();
247
248 $pathname = rtrim($pathname, '/');
0b0bfa93 249 if (!is_readable($archivefile)) {
17d9269f 250 return false;
251 }
e462f46b 252 $ziparch = new zip_archive();
0b0bfa93 253 if (!$ziparch->open($archivefile, file_archive::OPEN)) {
17d9269f 254 return false;
255 }
256
0b0bfa93 257 foreach ($ziparch as $info) {
258 $size = $info->size;
259 $name = $info->pathname;
17d9269f 260
17d9269f 261 if ($name === '' or array_key_exists($name, $processed)) {
e462f46b
FM
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.
17d9269f 266 continue;
267 }
268
0b0bfa93 269 if ($info->is_directory) {
17d9269f 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 }
288
289 $parts = explode('/', trim($name, '/'));
290 $filename = array_pop($parts);
291 $newdir = rtrim($pathname.'/'.implode('/', $parts), '/');
292
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 }
299
300 $newfile = "$newdir/$filename";
301 if (!$fp = fopen($newfile, 'wb')) {
302 $processed[$name] = 'Can not write target file'; // TODO: localise
303 continue;
304 }
0b0bfa93 305 if (!$fz = $ziparch->get_stream($info->index)) {
17d9269f 306 $processed[$name] = 'Can not read file from zip archive'; // TODO: localise
307 fclose($fp);
308 continue;
309 }
310
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 }
b1897a6d 325 $ziparch->close();
17d9269f 326 return $processed;
327 }
328
329 /**
01b4040a 330 * Unzip file to given file path (real OS filesystem), existing files are overwritten.
d2b7803e
DC
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
17d9269f 341 */
64f93798 342 public function extract_to_storage($archivefile, $contextid, $component, $filearea, $itemid, $pathbase, $userid = NULL) {
17d9269f 343 global $CFG;
344
0b0bfa93 345 if (!is_string($archivefile)) {
aefe9734 346 return $archivefile->extract_to_storage($this, $contextid, $component, $filearea, $itemid, $pathbase, $userid);
17d9269f 347 }
348
7aa06e6d 349 check_dir_exists($CFG->tempdir.'/zip');
17d9269f 350
351 $pathbase = trim($pathbase, '/');
352 $pathbase = ($pathbase === '') ? '/' : '/'.$pathbase.'/';
353 $fs = get_file_storage();
354
355 $processed = array();
356
b1897a6d 357 $ziparch = new zip_archive();
0b0bfa93 358 if (!$ziparch->open($archivefile, file_archive::OPEN)) {
17d9269f 359 return false;
360 }
361
0b0bfa93 362 foreach ($ziparch as $info) {
363 $size = $info->size;
364 $name = $info->pathname;
17d9269f 365
366 if ($name === '' or array_key_exists($name, $processed)) {
367 //probably filename collisions caused by filename cleaning/conversion
368 continue;
369 }
370
0b0bfa93 371 if ($info->is_directory) {
17d9269f 372 $newfilepath = $pathbase.$name.'/';
64f93798 373 $fs->create_directory($contextid, $component, $filearea, $itemid, $newfilepath, $userid);
17d9269f 374 $processed[$name] = true;
375 continue;
376 }
377
378 $parts = explode('/', trim($name, '/'));
379 $filename = array_pop($parts);
380 $filepath = $pathbase;
381 if ($parts) {
382 $filepath .= implode('/', $parts).'/';
383 }
384
385 if ($size < 2097151) {
01b4040a 386 // Small file.
0b0bfa93 387 if (!$fz = $ziparch->get_stream($info->index)) {
17d9269f 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 }
402
64f93798 403 if ($file = $fs->get_file($contextid, $component, $filearea, $itemid, $filepath, $filename)) {
17d9269f 404 if (!$file->delete()) {
405 $processed[$name] = 'Can not delete existing file'; // TODO: localise
406 continue;
407 }
408 }
ac6f1a82 409 $file_record = new stdClass();
17d9269f 410 $file_record->contextid = $contextid;
64f93798 411 $file_record->component = $component;
17d9269f 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;
424
425 } else {
426 // large file, would not fit into memory :-(
7aa06e6d 427 $tmpfile = tempnam($CFG->tempdir.'/zip', 'unzip');
17d9269f 428 if (!$fp = fopen($tmpfile, 'wb')) {
d4858e5c 429 @unlink($tmpfile);
17d9269f 430 $processed[$name] = 'Can not write temp file'; // TODO: localise
431 continue;
432 }
0b0bfa93 433 if (!$fz = $ziparch->get_stream($info->index)) {
d4858e5c 434 @unlink($tmpfile);
17d9269f 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);
7c53e3e5 444 if (filesize($tmpfile) !== $size) {
17d9269f 445 $processed[$name] = 'Unknown error during zip extraction'; // TODO: localise
446 // something went wrong :-(
447 @unlink($tmpfile);
448 continue;
449 }
450
64f93798 451 if ($file = $fs->get_file($contextid, $component, $filearea, $itemid, $filepath, $filename)) {
17d9269f 452 if (!$file->delete()) {
d4858e5c 453 @unlink($tmpfile);
17d9269f 454 $processed[$name] = 'Can not delete existing file'; // TODO: localise
455 continue;
456 }
457 }
ac6f1a82 458 $file_record = new stdClass();
17d9269f 459 $file_record->contextid = $contextid;
64f93798 460 $file_record->component = $component;
17d9269f 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 }
b1897a6d 475 $ziparch->close();
17d9269f 476 return $processed;
477 }
c78a0558 478
479 /**
01b4040a 480 * Returns array of info about all files in archive.
d2b7803e 481 *
79c966cf 482 * @param string|file_archive $archivefile
c78a0558 483 * @return array of file infos
484 */
485 public function list_files($archivefile) {
486 if (!is_string($archivefile)) {
487 return $archivefile->list_files();
488 }
489
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 }
498
f8c532ea 499}