/** * Extracts or lists the archive depending on $this->listmode. * * @param tgz_extractor_handler $handler Optional handler * @param file_progress $progress Optional progress reporting * @throws moodle_exception If there is any error processing the archive */ protected function extract_or_list(tgz_extractor_handler $handler = null, file_progress $progress = null) { // Open archive. if ($this->storedfile) { $gz = $this->storedfile->get_content_file_handle(stored_file::FILE_HANDLE_GZOPEN); // Estimate number of read-buffers (64KB) in file. Guess that the // uncompressed size is 2x compressed size. Add one just to ensure // it's non-zero. $estimatedbuffers = $this->storedfile->get_filesize() * 2 / self::READ_BLOCK_SIZE + 1; } else { $gz = gzopen($this->ospath, 'rb'); $estimatedbuffers = filesize($this->ospath) * 2 / self::READ_BLOCK_SIZE + 1; } if (!$gz) { throw new moodle_exception('errorprocessingarchive', '', '', null, 'Failed to open gzip file'); } // Calculate how much progress to report per buffer read. $progressperbuffer = (int) (tgz_packer::PROGRESS_MAX / $estimatedbuffers); // Process archive in 512-byte blocks (but reading 64KB at a time). $buffer = ''; $bufferpos = 0; $bufferlength = 0; $this->numfiles = -1; $read = 0; $done = 0; $beforeprogress = -1; while (true) { if ($bufferpos == $bufferlength) { $buffer = gzread($gz, self::READ_BLOCK_SIZE); $bufferpos = 0; $bufferlength = strlen($buffer); if ($bufferlength == 0) { // EOF. break; } // Report progress if enabled. if ($progress) { if ($this->numfiles === -1) { // If we don't know the number of files, do an estimate based // on number of buffers read. $done += $progressperbuffer; if ($done >= tgz_packer::PROGRESS_MAX) { $done = tgz_packer::PROGRESS_MAX - 1; } $progress->progress($done, tgz_packer::PROGRESS_MAX); } else { // Once we know the number of files, use this. if ($beforeprogress === -1) { $beforeprogress = $done; } // Calculate progress as whatever progress we reported // before we knew how many files there were (might be 0) // plus a proportion of the number of files out of the // remaining progress value. $done = $beforeprogress + (int) ($this->donefiles / $this->numfiles * (tgz_packer::PROGRESS_MAX - $beforeprogress)); } $progress->progress($done, tgz_packer::PROGRESS_MAX); } } $block = substr($buffer, $bufferpos, tgz_packer::TAR_BLOCK_SIZE); if ($this->currentfile) { $this->process_file_block($block, $handler); } else { $this->process_header($block, $handler); } // When listing, if we read an index file, we abort archive processing. if ($this->mode === self::MODE_LIST_COMPLETE) { break; } $bufferpos += tgz_packer::TAR_BLOCK_SIZE; $read++; } // Close archive and finish. gzclose($gz); }
/** * Perform archiving file from file path. * * @param zip_archive $ziparch zip archive instance * @param string $archivepath file path to archive * @param string $file path name of the file * @param file_progress $progress Progress indicator callback or null if not required * @return bool success */ private function archive_pathname($ziparch, $archivepath, $file, file_progress $progress = null) { // Record progress each time this function is called. if ($progress) { $progress->progress(); } if (!file_exists($file)) { return false; } if (is_file($file)) { if (!is_readable($file)) { return false; } return $ziparch->add_file_from_pathname($archivepath, $file); } if (is_dir($file)) { if ($archivepath !== '') { $ziparch->add_directory($archivepath); } $files = new DirectoryIterator($file); foreach ($files as $file) { if ($file->isDot()) { continue; } $newpath = $archivepath . '/' . $file->getFilename(); $this->archive_pathname($ziparch, $newpath, $file->getPathname(), $progress); } unset($files); // Release file handles. return true; } }
/** * Writes a single tar file to the archive, including its header record and * then the file contents. * * @param resource $gz Gzip file * @param string $archivepath Full path of file within archive * @param string|resource $file Full path of file on disk or file handle or null if none * @param int $size Size or 0 for directories * @param int|string $mtime Time or ? if unknown * @param string $content Actual content of file to write (null if using $filepath) * @param file_progress $progress Progress indicator or null if none * @param int $done Value for progress indicator * @return bool True if OK * @throws coding_exception If names aren't valid */ protected function write_tar_entry($gz, $archivepath, $file, $size, $mtime, $content = null, file_progress $progress = null, $done = 0) { // Header based on documentation of POSIX ustar format from: // http://www.freebsd.org/cgi/man.cgi?query=tar&sektion=5&manpath=FreeBSD+8-current . // For directories, ensure name ends in a slash. $directory = false; if ($size === 0 && is_null($file)) { $directory = true; if (!preg_match('~/$~', $archivepath)) { $archivepath .= '/'; } $mode = '755'; } else { $mode = '644'; } // Split archivepath into name and prefix. $name = $archivepath; $prefix = ''; while (strlen($name) > 100) { $slash = strpos($name, '/'); if ($slash === false) { throw new coding_exception('Name cannot fit length restrictions (> 100 characters): ' . $archivepath); } if ($prefix !== '') { $prefix .= '/'; } $prefix .= substr($name, 0, $slash); $name = substr($name, $slash + 1); if (strlen($prefix) > 155) { throw new coding_exception('Name cannot fit length restrictions (path too long): ' . $archivepath); } } // Checksum performance is a bit slow because of having to call 'ord' // lots of times (it takes about 1/3 the time of the actual gzwrite // call). To improve performance of checksum calculation, we will // store all the non-zero, non-fixed bytes that need adding to the // checksum, and checksum only those bytes. $forchecksum = $name; // struct header_posix_ustar { // char name[100]; $header = str_pad($name, 100, ""); // char mode[8]; // char uid[8]; // char gid[8]; $header .= '0000' . $mode . "00000000000000"; $forchecksum .= $mode; // char size[12]; $octalsize = decoct($size); if (strlen($octalsize) > 11) { throw new coding_exception('File too large for .tar file: ' . $archivepath . ' (' . $size . ' bytes)'); } $paddedsize = str_pad($octalsize, 11, '0', STR_PAD_LEFT); $forchecksum .= $paddedsize; $header .= $paddedsize . ""; // char mtime[12]; if ($mtime === '?') { // Use a default timestamp rather than zero; GNU tar outputs // warnings about zeroes here. $mtime = self::DEFAULT_TIMESTAMP; } $octaltime = decoct($mtime); $paddedtime = str_pad($octaltime, 11, '0', STR_PAD_LEFT); $forchecksum .= $paddedtime; $header .= $paddedtime . ""; // char checksum[8]; // Checksum needs to be completed later. $header .= ' '; // char typeflag[1]; $typeflag = $directory ? '5' : '0'; $forchecksum .= $typeflag; $header .= $typeflag; // char linkname[100]; $header .= str_pad('', 100, ""); // char magic[6]; // char version[2]; $header .= "ustar00"; // char uname[32]; // char gname[32]; // char devmajor[8]; // char devminor[8]; $header .= str_pad('', 80, ""); // char prefix[155]; // char pad[12]; $header .= str_pad($prefix, 167, ""); $forchecksum .= $prefix; // }; // We have now calculated the header, but without the checksum. To work // out the checksum, sum all the bytes that aren't fixed or zero, and add // to a standard value that contains all the fixed bytes. // The fixed non-zero bytes are: // // '000000000000000000 ustar00' // mode (except 3 digits), uid, gid, checksum space, magic number, version // // To calculate the number, call the calculate_checksum function on the // above string. The result is 1775. $checksum = 1775 + self::calculate_checksum($forchecksum); $octalchecksum = str_pad(decoct($checksum), 6, '0', STR_PAD_LEFT) . " "; // Slot it into place in the header. $header = substr($header, 0, 148) . $octalchecksum . substr($header, 156); if (strlen($header) != self::TAR_BLOCK_SIZE) { throw new coding_exception('Header block wrong size!!!!!'); } // Awesome, now write out the header. gzwrite($gz, $header); // Special pre-handler for OS filename. if (is_string($file)) { $file = fopen($file, 'rb'); if (!$file) { return false; } } if ($content !== null) { // Write in-memory content if any. if (strlen($content) !== $size) { throw new coding_exception('Mismatch between provided sizes: ' . $archivepath); } gzwrite($gz, $content); } else { if ($file !== null) { // Write file content if any, using a 64KB buffer. $written = 0; $chunks = 0; while (true) { $data = fread($file, 65536); if ($data === false || strlen($data) == 0) { break; } $written += gzwrite($gz, $data); // After every megabyte of large files, update the progress // tracker (so there are no long gaps without progress). $chunks++; if ($chunks == 16) { $chunks = 0; if ($progress) { // This call always has the same values, but that gives // the tracker a chance to indicate indeterminate // progress and output something to avoid timeouts. $progress->progress($done, self::PROGRESS_MAX); } } } fclose($file); if ($written !== $size) { throw new coding_exception('Mismatch between provided sizes: ' . $archivepath . ' (was ' . $written . ', expected ' . $size . ')'); } } else { if ($size != 0) { throw new coding_exception('Missing data file handle for non-empty file'); } } } // Pad out final 512-byte block in file, if applicable. $leftover = self::TAR_BLOCK_SIZE - $size % self::TAR_BLOCK_SIZE; if ($leftover == 512) { $leftover = 0; } else { gzwrite($gz, str_pad('', $leftover, "")); } return true; }