zZzzZ.
home2
/
zrzvbdry
/
wiredsave.com
/
wp-content
/
plugins
/
backup
/
src
/
JetBackup
/
Archive
New Folder
Editing: Archive.php
Save
Cancel
<?php /* * * JetBackup @ package * Created By Idan Ben-Ezra * * Copyrights @ JetApps * https://www.jetapps.com * **/ namespace JetBackup\Archive; use JetBackup\Archive\File\File; use JetBackup\Archive\File\FileInfo; use JetBackup\Archive\File\GZFile; use JetBackup\Archive\File\RegFile; use JetBackup\Archive\Header\Header; use JetBackup\Archive\Header\Sparse\Sparse; use JetBackup\Archive\Header\Sparse\SparseRegion; use JetBackup\Archive\Scan\Scan; use JetBackup\DirIterator\DirIteratorFile; use JetBackup\Entities\Util; use JetBackup\Exception\ArchiveException; use JetBackup\Exception\VanishedException; use JetBackup\Factory; use JetBackup\JetBackup; use JetBackup\Log\LogController; use JetBackup\Queue\QueueItem; class Archive { const ARCHIVE_TEMP_DATA_FILE = '.tar_working_file'; const ARCHIVE_EXT = '.tar'; const OPT_COMPRESSED = 1<<1; const OPT_IGNORE_VANISHED = 1<<2; const OPT_SPARSE = 1<<3; const OPT_DEBUG = 1<<4; const LOG_TYPE_INFO = 1; const LOG_TYPE_WARRING = 2; const LOG_TYPE_ERROR = 3; const LOG_TYPE_DEBUG = 4; protected const DEFAULT_COMPRESSION_LEVEL = 9; protected const FD_TYPE_READ = 1; protected const FD_TYPE_WRITE = 2; const BLOCK_SIZE = 512; const NULL_CHAR = "\0"; const TRUE_CHAR = "\x01"; protected $_filename; /** @var callable */ private $_exclude_callback; /** @var callable */ private $_progress_bar_callback; /** @var callable */ private $_logger; /** @var callable */ private $_extract_file_callback; /** @var callable */ private $_create_file_callback; private $_opt; private $_compress_level; protected ?File $_file = null; protected $_fd_type; private bool $_append; private LogController $_logController; /** * @var mixed|string|null */ private $_workspace; /** * @param string $archive_filename * @param bool $append * @param int $options * @param int $compress_level * @param null $workspace * * @throws ArchiveException */ public function __construct( string $archive_filename, bool $append = false, int $options=0, int $compress_level=self::DEFAULT_COMPRESSION_LEVEL, $workspace=null) { $this->_filename = $archive_filename; $this->_compress_level = $compress_level; $this->_append = $append; $this->_logController = new LogController(); if (!$workspace) $workspace = Factory::getLocations()->getTempDir(); $this->_workspace = $workspace; $this->_setOptions($options); if ($this->isCompressed()) { if(!function_exists('gzopen')) throw new ArchiveException('No gzip support available'); if ($this->_compress_level < 1 || $this->_compress_level > 9) throw new ArchiveException('Compression level should be between 1 and 9'); } } public static function isTar($path) : bool { return str_ends_with($path, Archive::ARCHIVE_EXT); } public static function isGzCompressed($path): bool { return str_ends_with($path, '.gz'); } /** * @param bool $append * * @return void */ public function setAppend(bool $append) { $this->_append = $append; } /** * @return bool */ public function isAppend():bool { return $this->_append; } /** * @param callable $logger * * @return void */ public function setLogController(LogController $logController): void { $this->_logController = $logController; } /** * @param callable $callback * * @return void */ public function setProgressBarCallback(callable $callback): void { $this->_progress_bar_callback = $callback; } /** * @param callable $callback * * @return void */ public function setExcludeCallback(callable $callback): void { $this->_exclude_callback = $callback; } /** * @param callable $callback * * @return void */ public function setExtractFileCallback(callable $callback): void { $this->_extract_file_callback = $callback; } /** * @param callable $callback * * @return void */ public function setCreateFileCallback(callable $callback): void { $this->_create_file_callback = $callback; } /** * @param int $file * * @return void */ protected function increaseProgressBar(int $file=1):void { if(!$this->_progress_bar_callback) return; call_user_func($this->_progress_bar_callback, $file); } /** * @param string $message * @param string $subMessage * @param int $totalSubItems * @param int $currentSubItems * * @return void */ protected function extractFileCallback(string $message, string $subMessage, int $totalSubItems, int $currentSubItems) { if(!$this->_extract_file_callback) return; call_user_func($this->_extract_file_callback, $message, $subMessage, $totalSubItems, $currentSubItems); } /** * @param Header $header * * @return void */ protected function createFileCallback(Header $header) { if(!$this->_create_file_callback) return; call_user_func($this->_create_file_callback, $header); } private function _setOptions(int $options) { $this->_opt = $options; } private function _getOptions(): int { return $this->_opt; } private function _addOption(int $option) { $this->_setOptions($this->_getOptions() | $option); } private function _isOption(int $option): bool { return !!($this->_getOptions() & $option); } /** * @return bool */ protected function isIgnoreVanished(): bool { return $this->_isOption(self::OPT_IGNORE_VANISHED); } /** * @return bool */ protected function isCompressed(): bool { return $this->_isOption(self::OPT_COMPRESSED); } /** * @return bool */ protected function isSparse(): bool { return $this->_isOption(self::OPT_SPARSE); } /** * @return bool */ protected function isDebug(): bool { return $this->_isOption(self::OPT_DEBUG); } /** * @param int $type * * @return void * @throws ArchiveException */ protected function _checkFD($type): void { if(!$this->_file || $this->_fd_type == $type) return; switch($this->_fd_type) { case self::FD_TYPE_READ: throw new ArchiveException("file is already open reading. you can't write to this file"); case self::FD_TYPE_WRITE: throw new ArchiveException("file is already open writing. you can't read from this file"); } } /** * @param int $type * * @return void * @throws ArchiveException */ protected function _createFd($type): void { $this->_checkFD($type); if($this->_file) return; if(!$this->isAppend() && $type == self::FD_TYPE_WRITE && DirIteratorFile::safe_filesize($this->_filename) && DirIteratorFile::safe_filesize($this->_filename) > 0) throw new ArchiveException("The archive file isn't empty, you can't open it for writing"); $this->_fd_type = $type; $file_info = pathinfo($this->_filename); if($this->isAppend()) { $this->_logController->logDebug('Tar _createFd: Append TRUE'); if($type == self::FD_TYPE_WRITE) { if(!file_exists($this->_filename)) touch($this->_filename); $this->_file = new RegFile($this->_filename, 'rb+'); $_file_size = DirIteratorFile::safe_filesize($this->_filename); $this->_logController->logDebug('Tar _createFd: FD_TYPE_WRITE -> Reset position based on size, seek -> ' . $_file_size); $this->_file->seek($_file_size); } else { $this->_logController->logDebug('Tar _createFd: NEW FD Opened'); $this->_file = new RegFile($this->_filename, 'rb'); } } elseif ($file_info['extension'] === 'gz') { $this->_logController->logDebug('Tar _createFd: [NOT Append] Extension GZ found, opening new FD'); $this->_file = new GZFile($this->_filename, $type == self::FD_TYPE_WRITE ? 'wb' : 'rb'); } else { $this->_logController->logDebug('Tar _createFd: [NOT Append | Not GZ] Opening new FD'); $this->_file = new RegFile($this->_filename, $type == self::FD_TYPE_WRITE ? 'wb' : 'rb'); } } private function _commitInfoFile($info) { $lockfile = $this->_getInfoFile(); $swap_file = $lockfile . '.swap'; $resolvedPath = realpath($swap_file); //$this->_logController->logDebug("Resolved swap file path: $resolvedPath (length: " . strlen($resolvedPath) . ")"); //$this->_logController->logDebug("Commit Info File: Lock file path is $lockfile"); //$this->_logController->logDebug("Commit Info File: Swap file path is $swap_file"); // Check if directory is writable if (!is_writable(dirname($swap_file))) { throw new ArchiveException("Directory is not writable: " . dirname($swap_file)); } // Check if swap file exists and is writable if (file_exists($swap_file) && !is_writable($swap_file)) { throw new ArchiveException("Swap file is not writable: " . $swap_file); } // Open the file for writing $fp = fopen($swap_file, 'c'); if ($fp === false) { throw new ArchiveException('Could not open info swap file for writing'); } //$this->_logController->logDebug("Commit Info File: Swap file opened successfully"); // Lock the file if (!flock($fp, LOCK_EX)) { fclose($fp); throw new ArchiveException('Could not lock info swap file'); } //$this->_logController->logDebug("Commit Info File: Swap file locked successfully"); // Encode the data to JSON $json_data = json_encode($info); if ($json_data === false) { $json_error = json_last_error_msg(); //$this->_logController->logDebug("Commit Info File: Failed to encode JSON. Error: $json_error"); flock($fp, LOCK_UN); fclose($fp); throw new ArchiveException("Failed to encode JSON data: $json_error"); } // Write the JSON data to the file $bytes_written = fwrite($fp, $json_data); if ($bytes_written === false || $bytes_written < strlen($json_data)) { flock($fp, LOCK_UN); fclose($fp); throw new ArchiveException("Could not write full JSON data to the swap file: $swap_file"); } //$this->_logController->logDebug("Commit Info File: Data written to swap file successfully"); // Flush and unlock the file fflush($fp); flock($fp, LOCK_UN); fclose($fp); //$this->_logController->logDebug("Commit Info File: Swap file flushed, unlocked, and closed"); // Remove the old info file if (file_exists($lockfile)) { if (!unlink($lockfile)) { throw new ArchiveException('Could not remove old info file'); } //$this->_logController->logDebug("Commit Info File: Old info file removed successfully"); } // Rename the swap file to the main file if (!rename($swap_file, $lockfile)) { throw new ArchiveException('Could not rename info swap file to main file'); } //$this->_logController->logDebug("Commit Info File: Swap file renamed to main file successfully"); } private function _getInfo() { $lockfile = $this->_getInfoFile(); $swap_file = $lockfile . '.swap'; $data = null; if (file_exists($lockfile)) { $fp = fopen($lockfile, 'r'); if ($fp !== false) { if (flock($fp, LOCK_SH)) { $data = json_decode(file_get_contents($lockfile)); flock($fp, LOCK_UN); } fclose($fp); } } if (!$data && file_exists($swap_file)) { $fp = fopen($swap_file, 'r'); if ($fp !== false) { if (flock($fp, LOCK_SH)) { $data = json_decode(file_get_contents($swap_file)); flock($fp, LOCK_UN); } fclose($fp); } if ($data && !rename($swap_file, $lockfile)) { throw new ArchiveException('Could not copy info swap file to main file'); } } if (file_exists($swap_file)) { unlink($swap_file); } return $data; } private function _getInfoFile(): string { return $this->_workspace . JetBackup::SEP . self::ARCHIVE_TEMP_DATA_FILE . '_' . md5($this->_filename) . '.tmp'; } /** * @throws ArchiveException */ private function _closeInfoFile() { $lockfile = $this->_getInfoFile(); $swap_file = $lockfile . '.swap'; if (file_exists($lockfile) && !unlink($lockfile)) { throw new ArchiveException('Could not remove info file'); } if (file_exists($swap_file) && !unlink($swap_file)) { throw new ArchiveException('Could not remove info swap file'); } } public function appendFileChunked(DirIteratorFile $fileInstance, $as='', ?callable $callback=null, $chunkSize = 1024) { $file = new \JetBackup\Filesystem\File($fileInstance->getName()); $this->_createFd(self::FD_TYPE_WRITE); // Load info if file exists $info = $this->_getInfo(); // If no info file found, or it's not the same file, create new info file if (!$info || !isset($info->file->name) || $info->file->name != $file->path()) { //$this->_logController->logDebug('Creating new info file or resetting info due to file mismatch'); if (!$info) { $this->_logController->logDebug('Tar appendFileChunked: No Chunk info data found (Adding new file)'); $archive_position = $fileInstance->getArchivePosition(); if ($archive_position !== null) { $this->_logController->logDebug('Tar appendFileChunked: Moving seek to pointer -> ' . $archive_position); $this->_file->seek($archive_position); } } if ($info && isset($info->file->name) && $info->file->name != $file->path()) { $this->_logController->logDebug('Tar appendFileChunked: Restarting existing file with valid info file'); $this->_logController->logDebug('Tar appendFileChunked: Moving seek to pointer -> ' . $info->position->start); $this->_file->seek($info->position->start); } $info = new \stdClass(); $info->file = new \stdClass(); $info->file->name = $file->path(); $info->file->size = $file->size(); $info->file->mtime = $file->mtime(); $info->file->changed = 0; $info->file->read = 0; $info->position = new \stdClass(); $info->position->start = $this->_file->tell(); $info->position->current = $info->position->start; $info->headers = false; $this->_commitInfoFile($info); } $this->_logController->logMessage('Tar appendFileChunked position: '. $this->_file->tell()); if (filesize($this->_filename) > 0 && $this->_file->tell() == 0) { $msg = 'Error: Tar position is zero with active tar file'; $this->_logController->logMessage($msg); throw new ArchiveException($msg); } // If file has changed while working on it, start from the beginning if ($info->file->size != $file->size() || $info->file->mtime != $file->mtime()) { $this->_logController->logDebug('Tar appendFileChunked: File ' . basename($file->path()) . ' changed, resetting positions'); // If this file changes more than 3 times, throw exception if ($info->file->changed >= 3) throw new ArchiveException("Tar appendFileChunked: File changed too many times"); $this->_file->seek($info->position->start); $info->file->size = $file->size(); $info->file->mtime = $file->mtime(); $info->file->changed++; $info->file->read = 0; $info->position->current = $info->position->start; $info->headers = false; $this->_commitInfoFile($info); } $this->_file->seek($info->position->current); if (!$info->headers) { $header = $this->writeHeader(FileInfo::fromPath($file->path(), $as)); $info->position->current = $this->_file->tell(); $info->headers = true; $this->_commitInfoFile($info); $this->createFileCallback($header); } // Not a file, nothing to do if (!($file->isFile() && !$file->isLink())) { // Reading is done, remove info file $this->_closeInfoFile(); return; } $fd = fopen($file->path(), 'r'); if (!$fd) throw new ArchiveException('Tar appendFileChunked: Could not open file for reading: ' . $file->path()); if ($info->file->read) fseek($fd, $info->file->read); while (!feof($fd)) { // Check if we need to stop if ($callback && call_user_func($callback)) { fclose($fd); return; } $read = $this->writeData(fread($fd, $chunkSize)); $info->file->read += $read; $info->position->current += $read; $this->_commitInfoFile($info); } fclose($fd); // Reading is done, remove info file $this->_closeInfoFile(); } /** * @param $data * * @return false|int * @throws ArchiveException */ protected function _write($data) { $this->_createFd(self::FD_TYPE_WRITE); return $this->_file->write($data); } /** * @param $length * * @return false|string * @throws ArchiveException */ protected function _read($length=self::BLOCK_SIZE) { $this->_createFd(self::FD_TYPE_READ); // we must read in block size chunks if($length != self::BLOCK_SIZE) $length = ceil($length / self::BLOCK_SIZE) * self::BLOCK_SIZE; return $this->_file->read($length); } protected function _eof() { $this->_createFd(self::FD_TYPE_READ); return $this->_file->eof(); } /** * @return void */ protected function _close(): void { if($this->_file) { $this->_file->close(); $this->_file = null; } } /** * @param string $data * * @return int * @throws ArchiveException */ public function writeData($data): int { $output = 0; for ($i = 0; $i < strlen($data); $i += self::BLOCK_SIZE) $output += $this->_write(pack("a" . self::BLOCK_SIZE, substr($data, $i, self::BLOCK_SIZE))); return $output; } /** * @return void * @throws ArchiveException */ public function save(): void { $this->writeData(''); $this->writeData(''); $this->_file->flush(); if($this->_file->tell() < DirIteratorFile::safe_filesize($this->_filename)) { $this->_file->truncate($this->_file->tell()); } $this->_close(); } /** * @param FileInfo $file * @param Sparse|null $sparse * * @return Header * @throws ArchiveException */ public function writeHeader(FileInfo $file, ?Sparse $sparse=null): Header { $header = new Header(); $header->setTypeFlag($file->getFlagType()); $header->setFilename($file->getPath()); $header->setSize($file->getSize(), false); $header->setMtime($file->getMtime(), false); $header->setMode(0777 & $file->getMode(), false); $header->setUid($file->getUid(), false); $header->setGid($file->getGid(), false); $header->setUname($file->getOwner()); $header->setGname($file->getGroup()); $header->setLinkName($file->getLink()); if($file->getDevMajor()) $header->setDevMajor($file->getDevMajor(), false); if($file->getDevMinor()) $header->setDevMinor($file->getDevMinor(), false); $header->setMagic("ustar"); $header->setSparse($sparse); if (strlen($file->getPath()) > Header::LENGTH_FILENAME) { $long_header = new Header(); $long_header->setTypeFlag(Header::GNUTYPE_LONGNAME); $long_header->setFilename('././@LongLink'); $long_header->setSize(strlen($file->getPath()), false); $long_header->setMagic("ustar"); $this->_write($long_header->pack()); $this->writeData($file->getPath()); $header->setFilename(substr($file->getPath(), 0, Header::LENGTH_FILENAME)); } $header->buildPrefix(); $this->_write($header->pack()); if($header->getTypeFlag() == Header::GNUTYPE_SPARSE && $header->getSparse()) $header->getSparse()->buildExtended(function($data) { $this->_write($data); }); return $header; } /** * @param string $path * @param bool $is_dir * * @return bool */ private function _isExcluded(string $path, bool $is_dir=false): bool { if(!$this->_exclude_callback) return false; return call_user_func($this->_exclude_callback, $path, $is_dir); } /** * @param string $path * @param FileInfo $info * * @return void * @throws ArchiveException */ private function _appendFile(string $path, FileInfo $info):void { if($info->getFlagType() != Header::REGTYPE) { $header = $this->writeHeader($info); $this->createFileCallback($header); return; } if($this->isSparse() && $info->getSize() >= self::BLOCK_SIZE && $info->getSparseness() < 1) { $info->setFlagType(Header::GNUTYPE_SPARSE); $sparse = new Sparse(); $sparse->setRealSize($info->getSize(), false); $file = new RegFile($path, 'rb'); $info->setSize(self::_buildSparse($file, $sparse)); $header = $this->writeHeader($info, $sparse); // write the data foreach($sparse->getRegions() as $region) { $offset = $region->getOffset(false); $numbytes = $region->getNumbytes(false); $file->seek($offset); if($numbytes) { $chunks = $numbytes / self::BLOCK_SIZE; for($i = 0; $i < $chunks; $i++) { $read = $file->read(self::BLOCK_SIZE); $this->writeData($read); } } } $file->close(); } else { $header = $this->writeHeader($info); $file = new RegFile($path, 'rb'); while (!$file->eof()) $this->writeData($file->read(self::BLOCK_SIZE)); $file->close(); } $this->createFileCallback($header); } /** * @param $path * @param FileInfo|null $info * * @return void * @throws ArchiveException */ public function appendFile($path, ?FileInfo $info=null): void { if(!file_exists($path)) throw new ArchiveException("Failed appending file '$path'. File not exists"); $this->increaseProgressBar(); if(!$info) $info = FileInfo::fromPath($path); else $info->setSize(filesize($path)); if($info->isSocket()) { $this->_logController->logMessage("Socket file '$path' ignored"); return; } $this->_appendFile($path, $info); } /** * @param FileInfo $info * @param string|null $data * * @return void * @throws ArchiveException */ public function appendData(FileInfo $info, ?string $data=null): void { $this->increaseProgressBar(); $info->setSize($data === null ? 0 : strlen($data)); $header = $this->writeHeader($info); if($data !== null) $this->writeData($data); $this->createFileCallback($header); } /** * @param string $source * @param string|bool $replace_source true or empty string will remove the source from the path, any other string will replace the source with the string * * @return void * @throws ArchiveException * @throws VanishedException */ public function appendDirectory($source, $replace_source=false): void { if(!file_exists($source) || !is_dir($source)) throw new ArchiveException("The provided directory not exists or not a directory"); if($this->_progress_bar_callback) { // get total $total = 0; $queue = [[opendir($source), $source]]; while($queue) { $dir = array_pop($queue); while(($entry = readdir($dir[0]))) { if($entry == '.' || $entry == '..') continue; $file = new \JetBackup\Filesystem\File($dir[1] . JetBackup::SEP . $entry); $clean_path = JetBackup::SEP . trim(substr($file->path(), strlen($source)), JetBackup::SEP); if($this->_isExcluded($clean_path, $file->isDir())) continue; if($file->isDir() && !$file->isLink()) { $queue[] = $dir; $queue[] = [opendir($file->path()), $file->path()]; continue 2; } // count files $total++; } // count directories $total++; } $this->increaseProgressBar($total); } $queue = [new Scan($source, $source)]; while($queue) { $dir = array_pop($queue); while($entry = $dir->next()) { $path = $entry->getFullPath(); if($entry->isSocket()) { $this->_logController->logMessage("Socket file '$path' ignored"); continue; } // Path is excluded, skip /** * We changed the way we are including and excluding files * Previously we had to scan the entire disk to determine if the file is included or excluded * We change it to use the same algorithm as rsync, The entire path must be included in order to be backed up * e.g. if we want to backup "/a/b/c/d" we must provide the following include list "/a", "/a/b", "/a/b/c", "/a/b/c/d/***" */ $clean_path = JetBackup::SEP . trim($entry->getCleanPath(), JetBackup::SEP); if($this->_isExcluded($clean_path, $entry->isDir())) continue; if($entry->isDir()) { try { $newDir = new Scan($path, $source); } catch(VanishedException $e) { // This dir has vanished, do nothing and continue backup up $this->_logController->logMessage($e->getMessage(), self::LOG_TYPE_DEBUG); if(!$this->isIgnoreVanished()) throw $e; continue; } $newDir->setParent($dir); $queue[] = $dir; $queue[] = $newDir; $path = $entry->getFullPath(); if(is_string($replace_source) && $replace_source) $path = rtrim($replace_source, JetBackup::SEP) . JetBackup::SEP . $entry->getCleanPath(); else if((is_string($replace_source) && !$replace_source) || $replace_source) $path = $entry->getCleanPath(); $fileinfo = FileInfo::fromPath($entry->getFullPath(), $path); $header = $this->writeHeader($fileinfo); $this->createFileCallback($header); continue 2; } $dir->addSize($entry->getSize()); $path = $entry->getFullPath(); if(is_string($replace_source) && $replace_source) $path = rtrim($replace_source, JetBackup::SEP) . JetBackup::SEP . $entry->getCleanPath(); else if((is_string($replace_source) && !$replace_source) || $replace_source) $path = $entry->getCleanPath(); $fileinfo = FileInfo::fromPath($entry->getFullPath(), $path); $this->_appendFile($entry->getFullPath(), $fileinfo); } } } private static function _buildSparse(File $file, Sparse $sparse) { $size = 0; $offset = 0; $numbytes = 0; while (!$file->eof()) { $data = $file->read(self::BLOCK_SIZE); // check if all are nulls if(self::_isNull($data)) { if($numbytes) { $region = new SparseRegion(); $region->setOffset($offset, false); $region->setNumbytes($numbytes, false); $sparse->addRegion($region); $offset += $numbytes; } $numbytes = 0; $offset += self::BLOCK_SIZE; } else { $data_len = strlen($data); $numbytes += $data_len; $size += $data_len; } } $region = new SparseRegion(); $region->setOffset($offset, false); $region->setNumbytes($numbytes, false); $sparse->addRegion($region); return $size; } /** * @param string $data * * @return bool */ private static function _isNull(string $data): bool { for($i = 0; $i < strlen($data); $i++) if($data[$i] != self::NULL_CHAR) return false; return true; } /** * @param string $destination * * @return void * @throws ArchiveException */ public function extract( string $destination): void { $this->_logController->logMessage("[tar] Extracting archive file '$destination'"); $destination = rtrim($destination, JetBackup::SEP); if (!is_dir($destination)) throw new ArchiveException("Destination directory not exists '$destination'"); // Load progress info if available $info = $this->_getInfo() ?? (object) [ 'currentFile' => null, 'currentOffset' => 0, 'totalBytesExtracted' => 0 ]; $longname = null; $corrupted_header = false; // We found out that is some cases (e.g. when we have bad gz file) the feof will return true and the read will return false // In that case we are in infinite loop. while(!$this->_eof()) { if(($data = $this->_read()) === false) throw new ArchiveException("The file provided is corrupted"); if(trim($data) === '') continue; try { $header = Header::parse($data, function() { return $this->_read(); }); $corrupted_header = false; } catch (ArchiveException $e) { if(!$corrupted_header) $this->_logController->logError("Invalid header detected. Skipping corrupted data block."); $corrupted_header = true; continue; // Skip this header and move to the next } if($header->getTypeFlag() == Header::GNUTYPE_LONGNAME) { $longname = $this->_read($header->getSize(false)); continue; } if($longname) { $header->setFilename($longname); $longname = null; } if($this->isDebug()) self::printHeader($header); // Resume Logic Validation: Skip files that are already processed $filename = trim($header->getFilename()); $info->currentFile = $filename; $size = $header->getSize(false); $mode = $header->getMode(false); $mtime = $header->getMtime(false); $uname = trim($header->getUname()) ?: $header->getUid(false); $gname = trim($header->getGname()) ?: $header->getGid(false); $link = trim($header->getLinkName()); $devmajor = $header->getDevMajor(false); $devminor = $header->getDevMinor(false); if($this->_isExcluded($filename)) { // read and drop all file data if ($size > 0) $this->_read($size); continue; } // Replace Windows backslashes with standard forward slashes $filename = Util::normalizePath($filename); $output = $destination. JetBackup::SEP .$filename; if (file_exists($output) && is_readable($output) && DirIteratorFile::safe_filesize($output) == $size) { if ($size > 0) $this->_read($size); continue; } $directory = $header->getTypeFlag() == Header::DIRTYPE ? $output : dirname($output); if(!file_exists($directory) && !@mkdir($directory, 0777, true)) { $this->_logController->logError("Failed creating directory '$directory'"); continue; } $this->_logController->logDebug("Processing case: {$header->getTypeFlag()} for file: $filename"); switch($header->getTypeFlag()) { case Header::GNUTYPE_SPARSE: try { $file = new RegFile($output, 'ab'); // Open in append mode for resumption $regions = $header->getSparse()->getRegions(); foreach ($regions as $index => $region) { // Skip already processed regions if (isset($info->sparseRegion) && $info->sparseRegion > $index) { continue; } $offset = $region->getOffset(false); $numbytes = $region->getNumbytes(false); if (sizeof($regions) == 1 && !$numbytes) { $file->truncate($offset); break; } if ($numbytes > 0) { $file->seek($offset); $file->write($this->_read($numbytes), $numbytes); $info->sparseRegion = $index; // Save progress } else { $file->seek($offset-1); $file->write(self::NULL_CHAR); } } $file->close(); } catch (ArchiveException $e) { $this->_logController->logError("Failed creating sparse file '$output'. Error: " . $e->getMessage()); continue 2; } break; case Header::REGTYPE: case Header::AREGTYPE: try { if (file_exists($output) && (!is_readable($output) || !is_writable($output))) { $this->_logController->logError("Output file '$output' has bad permissions or is not accessible. Skipping."); $this->_read($size); // Skip the data block continue 2; } $file = new RegFile($output, 'ab'); if($size) { // Resume from the last offset $file->seek($info->currentOffset); $remaining = $size - $info->currentOffset; if ($remaining <= 0) { $info->currentFile = null; $info->currentOffset = 0; $this->_commitInfoFile($info); continue 2; } for ($i = 0; $i < floor($remaining / self::BLOCK_SIZE); $i++) { $file->write($this->_read(), self::BLOCK_SIZE); $info->currentOffset += self::BLOCK_SIZE; } if (($remaining % self::BLOCK_SIZE) != 0) { $file->write($this->_read(), $remaining % self::BLOCK_SIZE); $info->currentOffset += $remaining % self::BLOCK_SIZE; } } $file->close(); // File fully processed, reset current offset $info->currentOffset = 0; $info->currentFile = null; $info->totalBytesExtracted += $size; $this->_commitInfoFile($info); } catch(ArchiveException $e) { $this->_logController->logError("Failed creating file '$output'. Error: " . $e->getMessage()); $this->_read($size); // Skip the data block continue 2; } break; case Header::SYMTYPE: try { if (!function_exists('symlink') || !@symlink($link, $output)) { $this->_logController->logError("Failed creating symlink '$output' to target '$link'"); } else { $this->_logController->logMessage("Created symlink '$output' -> '$link'"); } $info->currentFile = null; $info->currentOffset = 0; $this->_commitInfoFile($info); } catch (ArchiveException $e) { $this->_logController->logError("Exception while creating symlink '$output': " . $e->getMessage()); } break; // Exit the switch and proceed with the next iteration of the loop case Header::CHRTYPE: if(!posix_mknod($output, POSIX_S_IFCHR, $devmajor, $devminor)) { $this->_logController->logError("Failed creating character special file '$output'"); continue 2; } break; case Header::BLKTYPE: if(!posix_mknod($output, POSIX_S_IFBLK, $devmajor, $devminor)) { $this->_logController->logError("Failed creating block special file '$output'"); continue 2; } break; case Header::FIFOTYPE: if(!posix_mkfifo($output, $mode)) { $this->_logController->logError("Failed creating fifo file '$output'"); continue 2; } break; case Header::DIRTYPE: try { // Ensure the directory exists if (!file_exists($output) && !@mkdir($output, 0755, true)) { $this->_logController->logError("Failed creating directory '$output'"); break; // Log the error and skip } // Set ownership, permissions, and modification time if(function_exists('chown')) @chown($output, $uname); if(function_exists('chgrp')) @chgrp($output, $gname); if(function_exists('touch')) @touch($output, $mtime); if(function_exists('chmod')) @chmod($output, $mode); // Log successful processing of the directory $this->_logController->logDebug("Processed directory: $output"); // Reset state for the next file $info->currentFile = null; $info->currentOffset = 0; $this->_commitInfoFile($info); } catch (ArchiveException $e) { $this->_logController->logError("Error processing directory '$output'. Error: " . $e->getMessage()); continue 2; // Skip to the next item } break; } if($header->getTypeFlag() != Header::SYMTYPE) { if(function_exists('chown')) @chown($output, $uname); if(function_exists('chgrp')) @chgrp($output, $gname); if(function_exists('touch')) @touch($output, $mtime); if(function_exists('chmod')) @chmod($output, $mode); } $this->extractFileCallback('Archive Extract', $header->getFilename(), DirIteratorFile::safe_filesize($this->_filename), $info->totalBytesExtracted); } // close FD $this->_close(); $this->_closeInfoFile(); } /** * @throws ArchiveException */ public function list() { $longlink = ''; $rows = []; while(!$this->_eof()) { $data = $this->_read(); if(trim($data) === '') continue; $header = Header::parse($data, function() { return $this->_read(); }); if($header->getTypeFlag() == Header::GNUTYPE_LONGNAME) { $size = $header->getSize(false); if($size > 0) $longlink = $this->_read($size); continue; } switch($header->getTypeFlag()) { default: $permissions = '-'; break; //case Header::LNKTYPE: $permissions = '-'; break; case Header::SYMTYPE: $permissions = 'l'; break; case Header::CHRTYPE: $permissions = 'c'; break; case Header::BLKTYPE: $permissions = 'b'; break; case Header::DIRTYPE: $permissions = 'd'; break; case Header::FIFOTYPE: $permissions = 'p'; break; case Header::CONTTYPE: $permissions = '?'; break; case Header::GNUTYPE_SPARSE: continue 2; } $digits = str_split(substr($header->getMode(), 4)); $modes = ['---', '--x', '-w-', '-wx', 'r--', 'r-x', 'rw-', 'rwx']; $permissions .= $modes[$digits[0]] . $modes[$digits[1]] . $modes[$digits[2]]; $rows[] = [ 'mode' => $permissions, 'uname' => $header->getUname() ?: $header->getUid(false), 'gname' => $header->getGname() ?: $header->getGid(false), 'size' => $header->getDevMajor(false) ? $header->getDevMajor(false).','.$header->getDevMinor(false) : $header->getSize(false), 'mtime' => date("Y-m-d H:i", $header->getMtime(false)), 'filename' => ($longlink ?: $header->getFilename()) . ($header->getLinkName() ? ' -> ' . $header->getLinkName() : ''), ]; $longlink = ''; if( $header->getTypeFlag() == Header::REGTYPE || $header->getTypeFlag() == Header::AREGTYPE || $header->getTypeFlag() == Header::GNUTYPE_SPARSE ) { // skip data $size = $header->getSize(false); $chunk = 1024; $read = 0; if($size > 0) { while($read < $size) { $size_left = $size - $read; $read_size = ($size_left < $chunk)? $size_left: $chunk; $this->_read($read_size); $read += $read_size; } } } } // close FD $this->_close(); $spaces = ['mode'=>0,'uname'=>0,'gname'=>0,'size'=>0,'mtime'=>0,'filename'=>0]; foreach($rows as $row) { foreach($row as $key => $value) { if(isset($spaces[$key]) && $spaces[$key] > strlen($value)) continue; $spaces[$key] = strlen($value); } } foreach($rows as $row) { echo sprintf("%s %s %s %s %s", str_pad($row['mode'], $spaces['mode']), str_pad($row['uname']. JetBackup::SEP .$row['gname'], $spaces['uname']+$spaces['gname']+1), //str_pad($row['gname'], $spaces['gname']), str_pad($row['size'], $spaces['size'], ' ', STR_PAD_LEFT), str_pad($row['mtime'], $spaces['mtime']), $row['filename'] ) . PHP_EOL; } } public function dumpHeaders() { //$corrupted_header = false; while(!$this->_eof()) { $data = $this->_read(); if(trim($data) === '') continue; try { $header = Header::parse($data, function() { return $this->_read(); }, true); } catch (ArchiveException $e) { continue; } if( $header->getTypeFlag() == Header::REGTYPE || $header->getTypeFlag() == Header::AREGTYPE || $header->getTypeFlag() == Header::GNUTYPE_SPARSE || $header->getTypeFlag() == Header::GNUTYPE_LONGNAME ) { // skip data $size = $header->getSize(false); $chunk = 1024; $read = 0; if($size > 0) { while($read < $size) { $size_left = $size - $read; $read_size = ($size_left < $chunk)? $size_left: $chunk; $this->_read($read_size); $read += $read_size; } } } } // close FD $this->_close(); } public static function printHeader(Header $header) { echo "FILENAME: {$header->getFilename()}" . PHP_EOL; echo "PERMISSIONS: {$header->getMode()} -> {$header->getMode(false)}" . PHP_EOL; echo "UID: {$header->getUid()} -> {$header->getUid(false)}" . PHP_EOL; echo "GID: {$header->getGid()} -> {$header->getGid(false)}" . PHP_EOL; echo "SIZE: {$header->getSize()} -> {$header->getSize(false)}" . PHP_EOL; echo "MTIME: {$header->getMtime()} -> {$header->getMtime(false)}" . PHP_EOL; echo "CHECKSUM: {$header->getChecksum()}" . PHP_EOL; echo "TYPEFLAG: {$header->getTypeFlag()}" . PHP_EOL; if(trim($header->getLinkName())) echo "LINK: {$header->getLinkName()}" . PHP_EOL; if(trim($header->getUname())) echo "OWNER: {$header->getUname()}" . PHP_EOL; if(trim($header->getGname())) echo "GROUP: {$header->getGname()}" . PHP_EOL; if(trim($header->getDevMajor(false))) echo "DEV MAJOR: {$header->getDevMajor()} -> {$header->getDevMajor(false)}" . PHP_EOL; if(trim($header->getDevMinor(false))) echo "DEV MINOR: {$header->getDevMinor()} -> {$header->getDevMinor(false)}" . PHP_EOL; if(trim($header->getPrefix())) { $sparse = $header->getSparse(); if($sparse) { echo "SPARSE DETAILS" . PHP_EOL; if($sparse->getAtime(false)) echo "- ATIME: {$sparse->getAtime()} -> {$sparse->getAtime(false)}" . PHP_EOL; if($sparse->getCtime(false)) echo "- CTIME: {$sparse->getCtime()} -> {$sparse->getCtime(false)}" . PHP_EOL; if($sparse->getOffset(false)) echo "- OFFSET: {$sparse->getOffset()} -> {$sparse->getOffset(false)}" . PHP_EOL; if($sparse->getLongName(false)) echo "- LONGNAME: {$sparse->getLongName()} -> {$sparse->getLongName(false)}" . PHP_EOL; if($sparse->getRealSize(false)) echo "- REALSIZE: {$sparse->getRealSize()} -> {$sparse->getRealSize(false)}" . PHP_EOL; foreach($sparse->getRegions() as $i => $region) { echo "- REGION $i OFFSET: {$region->getOffset()} -> {$region->getOffset(false)} | NUMBYTES: {$region->getNumbytes()} -> {$region->getNumbytes(false)}" . PHP_EOL; } } else { echo "PREFIX: {$header->getPrefix()}" . PHP_EOL; } } echo "***********************************" . PHP_EOL; } public function getFileFD(): ?File { $this->_createFd(self::FD_TYPE_WRITE); return $this->_file; } }