diff --git a/README.md b/README.md index 1fb4c39..fc1d5ce 100644 --- a/README.md +++ b/README.md @@ -35,11 +35,15 @@ $sevenZip->format('7z') - **Compression Level**: You can adjust the compression level using the `faster()`, `slower()`, `mx()`, and `ultra()` methods. - **Custom Flags**: You can add custom compression flags using the `addFlag()` method. - **Progress Callback**: You can set a progress callback using the `progress()` method to monitor the compression progress. +- **Tar Before**: You can enable option to create a tar archive before compressing using the `tarBefore()` method. This is useful for preserving file + permissions and attributes. > [!WARNING] > The format support depends on your system, architecture, etc. > You can always use `format()` method to set your custom format. +> Any set `format()` which starts with `tar.` will automatically enable `tarBefore()`. To disable add `->forceTarBefore(false)` + ### Extraction To extract an archive: @@ -51,6 +55,8 @@ $sevenZip->source('/path/to/archive.7z') ->extract(); ``` +> If the extracted file is a single tar archive, it will be extracted and the tar file deleted. To avoid this behavior, add `->autoUntar(false)` + ### Encryption You can encrypt the compressed archive using a password: @@ -139,14 +145,16 @@ if ($sevenZip->checkSupport(['zip', 'tar', '7z'])) { ## TODO / WIP - [x] Full support for add flags (7z switches) -- [ ] Add custom support for gz, xz, etc. by using tar flags -- [ ] Use tar to keep original file permissions and other attributes +- [x] Add custom support for gz, xz, etc. by using tar flags +- [x] Use tar to keep original file permissions and other attributes +- [x] Auto untar on extraction - [x] Filter files by patterns - [x] Encrypt and decrypt - [ ] Test files using 7z test command - [x] Detect supported formats by the OS - [x] Add built-in binaries for mac and linux - [x] ~~Use docker for PHPUnit tests~~ not needed with built-in binaries +- [ ] Use native zstd, brotli, and others as fallback for these formats ## Contributing @@ -277,6 +285,54 @@ Sets the compression level to faster. **Returns**: The current instance of the SevenZip class. +### `deleteSourceAfterExtract(bool $delete = true): self` + +Sets whether to delete the source archive after successful extraction. + +**Parameters** + +- `$delete`: Whether to delete the source archive after extraction. Default is `true`. + +**Returns**: The current instance of the SevenZip class. + +**Example** + +```php +$sevenZip->deleteSourceAfterExtract(); +``` + +### `fileInfo(): string` + +Retrieves information about the specified archive file. + +**Returns**: The file information output from the 7-Zip command. + +**Throws** + +- `InvalidArgumentException`: If the archive path is not set. + +**Example** + +```php +$info = $sevenZip->fileInfo(); +``` + +### `fileList(): string` + +Lists the contents of the specified archive file. + +**Returns**: The file list output from the 7-Zip command. + +**Throws** + +- `InvalidArgumentException`: If the archive path is not set. + +**Example** + +```php +$list = $sevenZip->fileList(); +``` + ### `flagrize(array $flagsAndValues): array` Formats flags and values into an array of strings suitable for passing to 7-Zip commands. @@ -733,6 +789,72 @@ Sets the target path for compression/extraction using a fluent interface. **Returns**: The current instance of the SevenZip class. +### `tarBefore(bool $keepFileInfo = true): self` + +Enables creating a tar archive before compressing, preserving file permissions and attributes by default. + +**Parameters** + +- `$keepFileInfo` (optional): Whether to preserve file permissions and attributes when creating the tar archive. Default is `true`. + +**Returns**: The current instance of the SevenZip class. + +**Example** + +```php +$sevenZip->tarBefore(); +``` + +### `forceTarBefore(bool $force): self` + +Sets whether to force creating a tar archive before compressing, regardless of the format. + +**Parameters** + +- `$force`: Whether to force creating a tar archive before compressing. + +**Returns**: The current instance of the SevenZip class. + +**Example** + +```php +$sevenZip->forceTarBefore(true); +``` + +### `setTarKeepFileInfo(bool $keepFileInfo): self` + +Sets whether to preserve file permissions and attributes when creating a tar archive before compressing. + +**Parameters** + +- `$keepFileInfo`: Whether to preserve file permissions and attributes. + +**Returns**: The current instance of the SevenZip class. + +**Example** + +```php +$sevenZip->keepFileInfoOnTar(false); +``` + +### `shouldForceTarBefore(): bool` + +Gets whether forcing tar before compression is enabled. + +**Returns**: Whether forcing tar before compression is enabled. + +### `isTarKeepFileInfo(): bool` + +Gets whether preserving file permissions and attributes when creating a tar archive is enabled. + +**Returns**: Whether preserving file permissions and attributes is enabled. + +### `wasAlreadyTarred(): bool` + +Checks if the source has already been tarred. + +**Returns**: Whether the source has already been tarred. + ### `ultra(): self` Configures maximum compression settings based on the specified format. diff --git a/src/SevenZip.php b/src/SevenZip.php index c42204c..fb83446 100644 --- a/src/SevenZip.php +++ b/src/SevenZip.php @@ -2,6 +2,7 @@ namespace Verseles\SevenZip; +use Exception; use Symfony\Component\Process\ExecutableFinder; use Symfony\Component\Process\Process; use Verseles\SevenZip\Exceptions\ExecutableNotFoundException; @@ -18,28 +19,42 @@ class SevenZip * These flags are used to suppress progress output and automatically confirm operations. * @var array */ - protected array $alwaysFlags = ['bsp1', 'y' => null]; + protected array $alwaysFlags = [ + "bsp1", // Show progress on stdout + "y", // Auto confirm + "sccUTF-8", // UTF-8 + // "ssp", // Do not modify 'Last Access Time' property of source files when archiving or hashing + ]; /** * Default compression flags for different formats. - * Each format has a set of specific flags that are used to optimize the compression for that format. * @var array */ - protected array $defaultCompressFlags = [ - 'zip' => ['tzip'], - '7z' => ['t7z', 'm0' => 'lzma2'], - 'lzma2' => ['t7z', 'm0' => 'lzma2'], - 'lz4' => ['t7z', 'm0' => 'lz4'], - 'lz5' => ['t7z', 'm0' => 'lz5'], - 'bz2' => ['t7z', 'm0' => 'bzip2'], - 'bzip2' => ['t7z', 'm0' => 'bzip2'], - 'zstd' => ['t7z', 'm0' => 'zstd'], - 'zst' => ['t7z', 'm0' => 'zstd'], - 'brotli' => ['t7z', 'm0' => 'brotli'], - 'br' => ['t7z', 'm0' => 'brotli'], - 'gzip' => ['t7z', 'm0' => 'gzip'], - 'tar' => ['ttar'], - ]; + protected array $formatFlags = ["t7z", "m0" => "lzma2"]; + + /** + * Force use of tar before compressing. + * @var bool + */ + protected bool $forceTarBefore = false; + + /** + * Whether to keep file permissions and attributes when creating a tar archive. + * @var bool + */ + protected bool $keepFileInfoOnTar = true; + + /** + * Whether the archive was already tarred. + * @var bool + */ + protected bool $alreadyTarred = false; + + /** + * When decompressing, whether to automatically untar the archive. + * @var bool + */ + protected bool $autoUntar = true; /** * Custom flags that can be added to the 7-Zip command. @@ -71,13 +86,13 @@ class SevenZip * The path to the target file or directory for compression or extraction. * @var string */ - protected string $targetPath; + protected ?string $targetPath = null; /** * The path to the source file or directory for compression or extraction. * @var string */ - protected string $sourcePath; + protected ?string $sourcePath = null; /** * The password to be used for encryption or decryption. @@ -93,12 +108,18 @@ class SevenZip */ protected ?bool $encryptNames = true; + protected int $timeout = 300; + + protected int $idleTimeout = 120; + + protected bool $deleteSourceAfterExtract = false; + /** * The encryption method to be used for ZIP archives. * * @var string Can be 'ZipCrypto' (not secure) or 'AES128' or 'AES192' or 'AES256' */ - protected string $zipEncryptionMethod = 'AES256'; + protected string $zipEncryptionMethod = "AES256"; /** * Constructs a new SevenZip instance. @@ -117,10 +138,14 @@ public function __construct(string|bool|null $sevenZipPath = null) // Set the 7-Zip executable path if (!file_exists($sevenZipPath)) { - throw new ExecutableNotFoundException('7-Zip binary not found: ' . $sevenZipPath); + throw new ExecutableNotFoundException( + "7-Zip binary not found: " . $sevenZipPath + ); } if (!is_executable($sevenZipPath)) { - throw new ExecutableNotFoundException('7-Zip binary not executable: ' . $sevenZipPath); + throw new ExecutableNotFoundException( + "7-Zip binary not executable: " . $sevenZipPath + ); } $this->setSevenZipPath($sevenZipPath); @@ -129,7 +154,7 @@ public function __construct(string|bool|null $sevenZipPath = null) if ($sevenZipPath === true) { // Try to automatically detect the 7-Zip executable $finder = new ExecutableFinder(); - $sevenZipPath = $finder->find('7z'); + $sevenZipPath = $finder->find("7z"); if ($sevenZipPath !== null) { $this->setSevenZipPath($sevenZipPath); } @@ -139,12 +164,14 @@ public function __construct(string|bool|null $sevenZipPath = null) $this->setSevenZipPath($this->usePackageProvided7ZipExecutable()); } - if ($this->getSevenZipPath() === null) { throw new ExecutableNotFoundException(); } - // Here some options not set in $alwaysFlags to let the user override it + // Here some options not set by default to let the user override it + + // Load 7z as default archive format + $this->format("7z"); // Multi-Threaded Mode ON by default $this->mmt(); @@ -190,42 +217,59 @@ public function usePackageProvided7ZipExecutable(): ?string $version = 2403; $os = match (PHP_OS_FAMILY) { - 'Darwin' => 'mac', - 'Linux' => 'linux', + "Darwin" => "mac", + "Linux" => "linux", default => null, }; - $arch = match (php_uname('m')) { - 'x86_64' => 'x64', - 'x86' => 'x86', - 'arm64', 'aarch64' => 'arm64', - 'arm' => 'arm', + $arch = match (php_uname("m")) { + "x86_64" => "x64", + "x86" => "x86", + "arm64", "aarch64" => "arm64", + "arm" => "arm", default => null, }; if ($os !== null && $arch !== null) { - return sprintf('%s/../bin/7z%d-%s%s', __DIR__, $version, $os, $os === 'mac' ? '' : '-' . $arch); + return sprintf( + "%s/../bin/7z%d-%s%s", + __DIR__, + $version, + $os, + $os === "mac" ? "" : "-" . $arch + ); } return null; } + /** + * Set the archive format. + * + * @param string $format The compression format to be used. + * @return $this The current instance of the SevenZip class. + */ + public function format(string $format): self + { + return $this->setFormat($format); + } + /** * Set the number of CPU threads to use for compression. * * @param int|bool|string $threads The number of CPU threads to use, or 'on' or 'off'. * @return $this The current instance of the SevenZip class. */ - public function mmt(int|bool|string $threads = 'on'): self + public function mmt(int|bool|string $threads = "on"): self { if ($threads === true) { - $threads = 'on'; + $threads = "on"; } - if ($threads === false || $threads === 0 || $threads === '0') { - $threads = 'off'; + if ($threads === false || $threads === 0 || $threads === "0") { + $threads = "off"; } - return $this->addFlag('mmt', $threads); + return $this->addFlag("mmt", $threads); } /** @@ -236,7 +280,11 @@ public function mmt(int|bool|string $threads = 'on'): self * @param bool $glued Whether the flag should be glued with the value instead use = between flag and value (optional). * @return $this The current instance of the SevenZip class. */ - public function addFlag(string $flag, string $value = null, bool $glued = false): self + public function addFlag( + string $flag, + string $value = null, + bool $glued = false + ): self { if ($glued && $value !== null) { $flag .= $value; @@ -289,10 +337,10 @@ public function encrypt(string $password): self */ public function notEncryptNames(): self { - if ($this->getFormat() === 'zip') { - $this->removeFlag('em'); + if ($this->getFormat() === "zip") { + $this->removeFlag("em"); } else { - $this->removeFlag('mhe'); + $this->removeFlag("mhe"); } return $this->setEncryptNames(false); @@ -305,19 +353,20 @@ public function notEncryptNames(): self */ public function getFormat(): string { - return $this->format ?? '7z'; + return $this->format ?? "7z"; } /** * Set the archive format. * - * @param string $format The compression format to be used. + * @param ?string $format The compression format to be used. * @return $this The current instance of the SevenZip class. */ - public function setFormat(string $format): self + public function setFormat(?string $format = null): self { - $this->format = $format; - return $this; + $this->format = $format ?? "7z"; + + return $this->setFormatFlags(); } /** @@ -348,222 +397,102 @@ public function decrypt(string $password): self } /** - * Compress a file or directory. - * - * @return string The output of the 7-Zip command. - * @throws \InvalidArgumentException If target path, or source path is not set. - * - */ - public function compress(): string - { - if (!$this->getTargetPath()) { - throw new \InvalidArgumentException('Archive file path (target) must be set'); - } - - if (!$this->getSourcePath()) { - throw new \InvalidArgumentException('File or directory path (source) must be set'); - } - - if ($this->getFormat() === 'zip') { - if (!$this->getFlag('mm')) { - $this->mm('Deflate64'); - } - - if ($this->getPassword()) { - $this->addFlag('mem', $this->getZipEncryptionMethod()); - } - } - - if ($this->getPassword() && $this->getEncryptNames() && $this->getFormat() !== 'zip') { - $this->addFlag('mhe'); - } - - if ($this->getPassword()) { - $this->addFlag('p', $this->getPassword(), glued: true); - } - - $command = [ - $this->sevenZipPath, - 'a', - ...$this->flagrize($this->getAlwaysFlags()), - ...$this->flagrize($this->getDefaultCompressFlags()), - ...$this->flagrize($this->getCustomFlags()), - $this->getTargetPath(), - $this->getSourcePath(), - ]; - - return $this->runCommand($command); - } - - /** - * Get the target path for compression/extraction. - * - * @return string The path to the target file or directory for compression or extraction. - */ - public function getTargetPath(): string - { - return $this->targetPath; - } - - /** - * Set the target path for compression/extraction. + * Exclude archive filenames from the current command/archive. * - * @param string $path The path to the target file or directory for compression or extraction. - * @return $this The current instance of the SevenZip class. - */ - public function setTargetPath(string $path): self - { - $this->targetPath = $path; - return $this; - } - - /** - * Get the source path for compression/extraction. + * @param string|array $fileRefs File references to exclude. Can be a wildcard pattern or a list file. + * @param bool|int $recursive Recurse type. Can be: true for 'r' (enabled), false for 'r-' (disabled), 0 for 'r0' (enabled for wildcards) + * @return $this * - * @return string The path to the source file or directory for compression or extraction. - */ - public function getSourcePath(): string - { - return $this->sourcePath; - } - - /** - * Set the source path for compression/extraction. + * @throws \InvalidArgumentException If the file reference is not a string or an array. * - * @param string $path The path to the source file or directory for compression or extraction. - * @return $this The current instance of the SevenZip class. + * @example + * $sevenZip->exclude('*.7z'); + * $sevenZip->exclude('exclude_list.txt', false); + * $sevenZip->exclude(['*.7z', '*.zip'], 0); */ - public function setSourcePath(string $path): self - { - $this->sourcePath = $path; - return $this; - } - - public function getFlag(string $flag): mixed + public function exclude( + string|array $fileRefs, + bool|int $recursive = true + ): self { - return $this->customFlags[$flag] ?? null; + return $this->includeOrExclude("x", $fileRefs, $recursive); } /** - * Set the compression method for ZIP format + * Add file references flag to the current command/archive. * - * @param string $method Sets a method: Copy, Deflate, Deflate64, BZip2, LZMA, PPMd. + * @param string $flag The flag prefix ('x' for exclude, 'i' for include). + * @param string|array $fileRefs File references to add. Can be a wildcard pattern or a list file. + * @param bool|int $recursive Recurse type. Can be: true for 'r' (enabled), false for 'r-' (disabled), 0 for 'r0' (enabled for wildcards) * @return $this - */ - public function mm(string $method): self - { - return $this->addFlag('mm', $method); - } - - /** - * Get the password to be used for encryption or decryption. - * - * @return ?string The password or null if not set. - */ - public function getPassword(): ?string - { - return $this->password; - } - - /** - * Set the password to be used for encryption or decryption. * - * @param string $password The password to be used. - * @return $this The current instance of the SevenZip class. + * @throws \InvalidArgumentException If the file reference is not a string or an array. */ - public function setPassword(string $password): self + protected function includeOrExclude( + string $flag, + string|array $fileRefs, + bool|int $recursive + ): self { - $this->password = $password; - return $this; - } + if (is_string($fileRefs)) { + $fileRefs = [$fileRefs]; + } - public function getZipEncryptionMethod(): string - { - return $this->zipEncryptionMethod; - } + $r = match ($recursive) { + 0 => "r0", + true => "r", + false => "r-", + }; - public function setZipEncryptionMethod(string $zipEncryptionMethod): SevenZip - { - $this->zipEncryptionMethod = $zipEncryptionMethod; - return $this; - } + foreach ($fileRefs as $fileRef) { + $t = file_exists($fileRef) ? "@" : "!"; - public function getEncryptNames(): ?bool - { - return $this->encryptNames; - } + $this->addFlag(flag: $flag . $r . $t, value: $fileRef, glued: true); + } - public function setEncryptNames(?bool $encryptNames): SevenZip - { - $this->encryptNames = $encryptNames; return $this; } /** - * Format flags and values into an array of strings suitable for passing to 7-Zip commands. + * Include archive filenames for the current command/archive. * - * @param array $array An associative array of flags and their corresponding values. - * If the value is null, the flag will be added without an equal sign. - * @return array An array of formatted flag strings. - */ - public function flagrize(array $array): array - { - $formattedFlags = []; - - foreach ($array as $flag => $value) { - if (is_numeric($flag)) { - // flag with no value - $flag = $value; - $value = null; - } - - $formattedFlag = '-' . $flag; - - if ($value !== null) { - $formattedFlag .= '=' . $value; - } - - $formattedFlags[] = $formattedFlag; - } - - return $formattedFlags; - } - - /** - * Get the always flags. + * @param string|array $fileRefs File references to include. Can be a wildcard pattern or a list file. + * @param bool|int $recursive Recurse type. Can be: true for 'r' (enabled), false for 'r-' (disabled), 0 for 'r0' (enabled for wildcards) + * @return $this * - * @return array The array of flags that are always used when running 7-Zip commands. + * @throws \InvalidArgumentException If the file reference is not a string or an array. + * + * @example + * $sevenZip->include('*.txt'); + * $sevenZip->include('include_list.txt', false); + * $sevenZip->include(['*.txt', '*.doc'], 0); */ - protected function getAlwaysFlags(): array + public function include( + string|array $fileRefs, + bool|int $recursive = true + ): self { - return $this->alwaysFlags; + return $this->includeOrExclude("i", $fileRefs, $recursive); } /** - * Set the always flags. + * Prints information about the 7-Zip executable. * - * @param array $alwaysFlags The array of flags that are always used when running 7-Zip commands. - * @return SevenZip The current instance of the SevenZip class. + * @return void */ - protected function setAlwaysFlags(array $alwaysFlags): SevenZip + public function info(): void { - $this->alwaysFlags = $alwaysFlags; - return $this; + print $this->getInfo(); } /** - * Get the default compression flags for the specified format. + * Retrieves information about the 7-Zip executable. * - * @param string|null $format Archive format (optional). - * @return array The default compression flags for the specified format. + * @return string The output of the command execution. */ - protected function getDefaultCompressFlags(?string $format = null): array + public function getInfo(): string { - if ($format !== null) { - $this->setFormat($format); - } - - return $this->defaultCompressFlags[$this->getFormat()] ?? ['t' . $this->getFormat()]; + return $this->runCommand([$this->getSevenZipPath(), "i"], secondary: true); } /** @@ -575,16 +504,29 @@ protected function getDefaultCompressFlags(?string $format = null): array */ protected function runCommand(array $command, bool $secondary = false): string { + +// echo "\nCommand: " . implode(" ", $command) . "\n"; $process = new Process($command); + $process->setTimeout($this->getTimeout()); + + $process->run( + $secondary + ? null + : function ($type, $buffer) use ($process) { + if ($type === Process::OUT) { + $this->parseProgress($buffer); + } - $process->run($secondary ? null : function ($type, $buffer) { - if ($type === Process::OUT) { - $this->parseProgress($buffer); + $process->checkTimeout(); } - }); + ); if (!$process->isSuccessful()) { - throw new \RuntimeException($process->getErrorOutput()); + throw new \RuntimeException( + "Command: " . implode(" ", $command) . "\n" . + "Output: " . $process->getOutput() . "\n" . + "Error Output: " . + $process->getErrorOutput()); } if (!$secondary) { @@ -595,6 +537,17 @@ protected function runCommand(array $command, bool $secondary = false): string return $process->getOutput(); } + public function getTimeout(): int + { + return $this->timeout; + } + + public function setTimeout(int $timeout): SevenZip + { + $this->timeout = $timeout; + return $this; + } + /** * Parse the progress from the command output. * @@ -608,7 +561,7 @@ protected function parseProgress(string $output): void $lines = explode("\n", $output); foreach ($lines as $line) { - if (preg_match('/(\d+)%\s+\d+/', $line, $matches)) { + if (preg_match("/(\d+)%\s+\d+/", $line, $matches)) { $progress = intval($matches[1]); $this->setProgress($progress); } @@ -639,7 +592,10 @@ public function setProgressCallback(callable $callback): self protected function setProgress(int $progress): void { - if ($this->getProgressCallback() !== null && $progress > $this->getLastProgress()) { + if ( + $this->getProgressCallback() !== null && + $progress > $this->getLastProgress() + ) { $this->setLastProgress($progress); call_user_func($this->getProgressCallback(), $progress); } @@ -674,122 +630,40 @@ protected function setLastProgress(int $lastProgress): self */ public function reset(): self { - $this->customFlags = []; - $this->progressCallback = null; - $this->lastProgress = -1; - $this->format = null; - $this->targetPath = ''; - $this->sourcePath = ''; - $this->password = null; + $this->customFlags = []; + $this->progressCallback = null; + $this->lastProgress = -1; + $this->format = null; + $this->targetPath = null; + $this->sourcePath = null; + $this->password = null; + $this->encryptNames = true; + $this->timeout = 300; + $this->idleTimeout = 120; + $this->zipEncryptionMethod = 'AES256'; + $this->forceTarBefore = false; + $this->keepFileInfoOnTar = true; + $this->alreadyTarred = false; + $this->autoUntar = true; + $this->deleteSourceAfterExtract = false; return $this; } /** - * Exclude archive filenames from the current command/archive. - * - * @param string|array $fileRefs File references to exclude. Can be a wildcard pattern or a list file. - * @param bool|int $recursive Recurse type. Can be: true for 'r' (enabled), false for 'r-' (disabled), 0 for 'r0' (enabled for wildcards) - * @return $this - * - * @throws \InvalidArgumentException If the file reference is not a string or an array. + * Checks if the given extension(s) are supported by the current 7-Zip installation. * - * @example - * $sevenZip->exclude('*.7z'); - * $sevenZip->exclude('exclude_list.txt', false); - * $sevenZip->exclude(['*.7z', '*.zip'], 0); + * @param string|array $extensions The extension or an array of extensions to check. + * @return bool Returns true if all the given extensions are supported, false otherwise. */ - public function exclude(string|array $fileRefs, bool|int $recursive = true): self + public function checkSupport(string|array $extensions): bool { - return $this->includeOrExclude('x', $fileRefs, $recursive); - } + $supportedExtensions = $this->getSupportedFormatExtensions(); - /** - * Add file references flag to the current command/archive. - * - * @param string $flag The flag prefix ('x' for exclude, 'i' for include). - * @param string|array $fileRefs File references to add. Can be a wildcard pattern or a list file. - * @param bool|int $recursive Recurse type. Can be: true for 'r' (enabled), false for 'r-' (disabled), 0 for 'r0' (enabled for wildcards) - * @return $this - * - * @throws \InvalidArgumentException If the file reference is not a string or an array. - */ - protected function includeOrExclude(string $flag, string|array $fileRefs, bool|int $recursive): self - { - if (is_string($fileRefs)) { - $fileRefs = [$fileRefs]; - } - - $r = match ($recursive) { - 0 => 'r0', - true => 'r', - false => 'r-', - }; - - foreach ($fileRefs as $fileRef) { - $t = file_exists($fileRef) ? '@' : '!'; - - $this->addFlag( - flag : $flag . $r . $t, - value: $fileRef, - glued: true - ); + if (is_string($extensions)) { + $extensions = [$extensions]; } - return $this; - } - - /** - * Include archive filenames for the current command/archive. - * - * @param string|array $fileRefs File references to include. Can be a wildcard pattern or a list file. - * @param bool|int $recursive Recurse type. Can be: true for 'r' (enabled), false for 'r-' (disabled), 0 for 'r0' (enabled for wildcards) - * @return $this - * - * @throws \InvalidArgumentException If the file reference is not a string or an array. - * - * @example - * $sevenZip->include('*.txt'); - * $sevenZip->include('include_list.txt', false); - * $sevenZip->include(['*.txt', '*.doc'], 0); - */ - public function include(string|array $fileRefs, bool|int $recursive = true): self - { - return $this->includeOrExclude('i', $fileRefs, $recursive); - } - - /** - * Prints information about the 7-Zip executable. - * - * @return void - */ - public function info(): void - { - print $this->getInfo(); - } - - /** - * Retrieves information about the 7-Zip executable. - * - * @return string The output of the command execution. - */ - public function getInfo(): string - { - return $this->runCommand([$this->getSevenZipPath(), 'i'], secondary: true); - } - - /** - * Checks if the given extension(s) are supported by the current 7-Zip installation. - * - * @param string|array $extensions The extension or an array of extensions to check. - * @return bool Returns true if all the given extensions are supported, false otherwise. - */ - public function checkSupport(string|array $extensions): bool - { - $supportedExtensions = $this->getSupportedFormatExtensions(); - - if (is_string($extensions)) $extensions = [$extensions]; - foreach ($extensions as $extension) { if (!in_array($extension, $supportedExtensions, true)) { return false; @@ -807,13 +681,13 @@ public function checkSupport(string|array $extensions): bool */ public function getSupportedFormatExtensions(?array $formats = null): array { - $formats ??= $this->getParsedInfo()['formats']; + $formats ??= $this->getParsedInfo()["formats"]; $extensions = []; foreach ($formats as $format) { - foreach ($format['extensions'] as $extension) { - $extension = preg_replace('/[^a-zA-Z0-9]/', '', $extension); - if ($extension !== '') { + foreach ($format["extensions"] as $extension) { + $extension = preg_replace("/[^a-zA-Z0-9]/", "", $extension); + if ($extension !== "") { $extensions[$extension] = $extension; } } @@ -839,10 +713,10 @@ protected function getParsedInfo(?string $output = null): array protected function parseInfoOutput(string $output): array { $data = [ - 'version' => '', - 'formats' => [], - 'codecs' => [], - 'hashers' => [], + "version" => "", + "formats" => [], + "codecs" => [], + "hashers" => [], ]; $lines = explode("\n", $output); @@ -850,23 +724,23 @@ protected function parseInfoOutput(string $output): array foreach ($lines as $line) { $line = trim($line); - if (str_starts_with($line, '7-Zip')) { - $data['version'] = $line; - } elseif (str_starts_with($line, 'Formats:')) { + if (str_starts_with($line, "7-Zip")) { + $data["version"] = $line; + } elseif (str_starts_with($line, "Formats:")) { continue; - } elseif (str_starts_with($line, 'Codecs:')) { + } elseif (str_starts_with($line, "Codecs:")) { break; } else { - $regex = '/(.+?)\s{2}([A-za-z0-9]+)\s+((?:[a-z0-9().~]+\s?)+)(.*)/mu'; + $regex = "/(.+?)\s{2}([A-za-z0-9]+)\s+((?:[a-z0-9().~]+\s?)+)(.*)/mu"; preg_match_all($regex, $line, $matches, PREG_SET_ORDER, 0); if (isset($matches[0]) && count($matches[0]) >= 3) { - $formatParts = array_map('trim', $matches[0]); + $formatParts = array_map("trim", $matches[0]); - $data['formats'][] = [ - 'flags' => $formatParts[1], - 'name' => $formatParts[2], - 'extensions' => explode(' ', $formatParts[3]), - 'signature' => $formatParts[4], + $data["formats"][] = [ + "flags" => $formatParts[1], + "name" => $formatParts[2], + "extensions" => explode(" ", $formatParts[3]), + "signature" => $formatParts[4], ]; } } @@ -876,22 +750,22 @@ protected function parseInfoOutput(string $output): array foreach ($lines as $line) { $line = trim($line); - if (str_starts_with($line, 'Codecs:')) { + if (str_starts_with($line, "Codecs:")) { $codecsStarted = true; continue; } if ($codecsStarted) { - if (str_starts_with($line, 'Hashers:')) { + if (str_starts_with($line, "Hashers:")) { break; } - $codecParts = preg_split('/\s+/', $line); + $codecParts = preg_split("/\s+/", $line); if (count($codecParts) >= 2) { - $data['codecs'][] = [ - 'flags' => $codecParts[0], - 'id' => $codecParts[1], - 'name' => $codecParts[2], + $data["codecs"][] = [ + "flags" => $codecParts[0], + "id" => $codecParts[1], + "name" => $codecParts[2], ]; } } @@ -901,18 +775,18 @@ protected function parseInfoOutput(string $output): array foreach ($lines as $line) { $line = trim($line); - if (str_starts_with($line, 'Hashers:')) { + if (str_starts_with($line, "Hashers:")) { $hashersStarted = true; continue; } if ($hashersStarted) { - $hasherParts = preg_split('/\s+/', $line); + $hasherParts = preg_split("/\s+/", $line); if (count($hasherParts) >= 3) { - $data['hashers'][] = [ - 'size' => (int)$hasherParts[0], - 'id' => $hasherParts[1], - 'name' => $hasherParts[2], + $data["hashers"][] = [ + "size" => (int)$hasherParts[0], + "id" => $hasherParts[1], + "name" => $hasherParts[2], ]; } } @@ -921,6 +795,37 @@ protected function parseInfoOutput(string $output): array return $data; } + public function tarBefore(bool $keepFileInfo = true): self + { + return $this + ->forceTarBefore(true) + ->keepFileInfoOnTar($keepFileInfo); + } + + /** + * Set whether to keep file info when using TAR before compression. + * + * @param bool $keep Whether to keep file info when using TAR before compression. + * @return $this The current instance of the SevenZip class. + */ + public function keepFileInfoOnTar(bool $keep): SevenZip + { + $this->keepFileInfoOnTar = $keep; + return $this; + } + + /** + * Set whether to force TAR before compression. + * + * @param bool $force Whether to force TAR before compression. + * @return $this The current instance of the SevenZip class. + */ + public function forceTarBefore(bool $force): SevenZip + { + $this->forceTarBefore = $force; + return $this; + } + /** * Extract an archive. * @@ -931,205 +836,853 @@ protected function parseInfoOutput(string $output): array public function extract(): string { if (!$this->getSourcePath()) { - throw new \InvalidArgumentException('Archive path (source) must be set'); + throw new \InvalidArgumentException("Archive path (source) must be set"); } if (!$this->getTargetPath()) { - throw new \InvalidArgumentException('Extract path (target) must be set'); + throw new \InvalidArgumentException("Extract path (target) must be set"); } // Set output path - $this->addFlag('o', $this->getTargetPath(), glued: true); + $this->addFlag("o", $this->getTargetPath(), glued: true); if ($this->getPassword()) { - $this->addFlag('p', $this->getPassword(), glued: true); + $this->addFlag("p", $this->getPassword(), glued: true); + } + + $forceUntar = false; + if ($this->shouldAutoUntar()) { + $fileList = $this->fileList(); + + $tarFile = $fileList[0]['path']; + $isTarFile = pathinfo($tarFile, PATHINFO_EXTENSION) === 'tar'; + $isSingleFile = count($fileList) === 1; + + $forceUntar = $isSingleFile && $isTarFile; } $command = [ $this->getSevenZipPath(), - 'x', + "x", ...$this->flagrize($this->getAlwaysFlags()), ...$this->flagrize($this->getCustomFlags()), $this->getSourcePath(), ]; - return $this->runCommand($command); - } + $shouldDeleteSourceAfterExtract = $this->shouldDeleteSourceAfterExtract(); + $sourcePath = $this->getSourcePath(); - /** - * Sets the source path for the compression or extraction operation. - * - * @param string $path The source path. - * @return static The current instance of the SevenZip class. - */ - public function source(string $path): self - { - return $this->setSourcePath($path); + + if ($forceUntar) { + $output = $this->executeUntarAfter($tarFile, $command); + } else { + $output = $this->runCommand($command); + } + + if ($shouldDeleteSourceAfterExtract) { + unlink($sourcePath); + } + + return $output; } /** - * Set the archive format. + * Get the source path for compression/extraction. * - * @param string $format The compression format to be used. - * @return $this The current instance of the SevenZip class. + * @return ?string The path to the source file or directory for compression or extraction. */ - public function format(string $format): self + public function getSourcePath(): ?string { - return $this->setFormat($format); + return $this->sourcePath; } /** - * Set the progress callback using a fluent interface. + * Set the source path for compression/extraction. * - * @param callable $callback The callback function to be called during the compression progress. + * @param string $path The path to the source file or directory for compression or extraction. * @return $this The current instance of the SevenZip class. */ - public function progress(callable $callback): self + public function setSourcePath(string $path): self { - return $this->setProgressCallback($callback); + $this->sourcePath = $path; + return $this; } /** - * Set the compression level to faster. + * Get the target path for compression/extraction. * - * @return $this The current instance of the SevenZip class. + * @return ?string The path to the target file or directory for compression or extraction. */ - public function faster(): self + public function getTargetPath(): ?string { - if ($this->getFormat() === 'zstd' || $this->getFormat() === 'zst') { - return $this->mx(0); - } - - return $this->mx(1); + return $this->targetPath; } /** - * Set the compression level using the -mx flag. + * Set the target path for compression/extraction. * - * @param int $level The compression level to be used. + * @param string $path The path to the target file or directory for compression or extraction. * @return $this The current instance of the SevenZip class. */ - public function mx(int $level): self + public function setTargetPath(string $path): self { - return $this->addFlag('mx', $level); + $this->targetPath = $path; + return $this; } /** - * Set the compression level to slower. + * Get the password to be used for encryption or decryption. * - * @return $this The current instance of the SevenZip class. + * @return ?string The password or null if not set. */ - public function slower(): self + public function getPassword(): ?string { - if ($this->getFormat() === 'zstd' || $this->getFormat() === 'zst') { - return $this->mx(22); - } - - return $this->mx(9); + return $this->password; } - /* - * Sets level of file analysis. - * - * @param int $level - * @return $this - */ - /** - * Configures maximum compression settings based on the specified format. + * Set the password for encryption or decryption. * - * @return static The current instance for method chaining. + * @param string $password The password to be used for encryption or decryption. + * @return $this The current instance of the SevenZip class. */ - public function ultra(): self + public function setPassword(string $password): self { - $this->mmt(true)->mx(9); - - return match ($this->getFormat()) { - 'zip' => $this->mm('Deflate64')->mfb(257)->mpass(15)->mmem(28), - 'gzip' => $this->mfb(258)->mpass(15), - 'bzip2' => $this->mpass(7)->md('900000b'), - '7z' => $this->m0('lzma2')->mfb(64)->ms(true)->md('32m'), - 'zstd', 'zst' => $this->mx(22), - default => $this, - }; + $this->password = $password; + return $this; } - public function mmem(int|string $size = 24) + public function shouldAutoUntar(): bool { - return $this->addFlag('mmem', $size); + return $this->autoUntar; } /** - * Set the number of passes for compression. + * List the files inside an archive. * - * @param int $number The number of passes for compression. - * @return $this The current instance of the SevenZip class. + * @return array The list of files inside the archive. + * @throws \InvalidArgumentException If the source path is not set. */ - public function mpass(int $number = 7): self + public function fileList(): array { - return $this->addFlag('mpass', $number); + return $this->fileInfo()['files'] ?? []; } /** - * Set the size of the Fast Bytes for the compression algorithm. + * Get information about an archive and its contents. * - * @param int $bytes The size of the Fast Bytes. The default value (when set) is 64. - * @return $this The current instance of the SevenZip class. + * @return array An array containing 'info' and 'files' keys with the archive information and file list. + * @throws \InvalidArgumentException If the source path is not set. */ - public function mfb(int $bytes = 64): self + public function fileInfo(): array { - return $this->addFlag('mfb', $bytes); - } + if (!$this->getSourcePath()) { + throw new \InvalidArgumentException('Archive path (source) must be set'); + } - public function md(string $size = '32m'): self - { - return $this->addFlag('md', $size); - } + if ($this->getPassword()) { + $this->addFlag('p', $this->getPassword(), glued: true); + } - public function ms(bool|string|int $on = true): self - { - return $this->addFlag('ms', $on ? 'on' : 'off'); + $command = [ + $this->getSevenZipPath(), + 'l', + ...$this->flagrize($this->getAlwaysFlags()), + ...$this->flagrize($this->getCustomFlags()), + $this->getSourcePath(), + ]; + + $output = $this->runCommand($command, secondary: true); + + return $this->parseFileInfoOutput($output); } /** - * Set the compression method. - * @param $method string The compression method to be used. - * @return $this + * Format flags and values into an array of strings suitable for passing to 7-Zip commands. + * + * @param array $array An associative array of flags and their corresponding values. + * If the value is null, the flag will be added without an equal sign. + * @return array An array of formatted flag strings. */ - public function m0($method): self + public function flagrize(array $array): array { - return $this->addFlag('m0', $method); + $formattedFlags = []; + + foreach ($array as $flag => $value) { + if (is_numeric($flag)) { + // flag with no value + $flag = $value; + $value = null; + } + + $formattedFlag = "-" . $flag; + + if ($value !== null) { + $formattedFlag .= "=" . $value; + } + + $formattedFlags[] = $formattedFlag; + } + + return $formattedFlags; } /** - * Configures no compression (copy only) settings based on the specified format. + * Get the always flags. * - * @return static The current instance for method chaining. + * @return array The array of flags that are always used when running 7-Zip commands. */ - public function copy(): self + protected function getAlwaysFlags(): array { - return $this->mmt(true)->mx(0)->m0('Copy')->mm('Copy')->myx(0); + return $this->alwaysFlags; } /** - * Sets file analysis level. + * Set the always flags. * - * @param int $level - * @return $this + * @param array $alwaysFlags The array of flags that are always used when running 7-Zip commands. + * @return SevenZip The current instance of the SevenZip class. */ - public function myx(int $level = 5): self + protected function setAlwaysFlags(array $alwaysFlags): SevenZip { - return $this->addFlag('myx', $level); + $this->alwaysFlags = $alwaysFlags; + return $this; } /** - * Set the target path for compression/extraction using a fluent interface. + * Parse the output of the 7z "l" command and return an array with archive information and file list. * - * @param string|null $path The path to the target file or directory for compression or extraction. - * @return $this The current instance of the SevenZip class. + * @param string $output The output of the 7z "l" command. + * @return array An array containing 'info' and 'files' keys with the archive information and file list. */ - public function target(?string $path): self + protected function parseFileInfoOutput(string $output): array { - return $this->setTargetPath($path); + $info = []; + $files = []; + + $lines = explode("\n", $output); + $part = ''; + +// var_dump($lines); + + foreach ($lines as $line) { + if ($part === '' && preg_match('/^--$/', $line)) { + $part = 'header'; + continue; + } elseif ($part === 'header' && preg_match('/Date\s+?Time\s+?Attr\s+?Size\s+?Compressed\s+?Name/', $line)) { + $part = 'files-jump1'; + continue; + } elseif ($part === 'files-jump1') { + $part = 'files'; + continue; + } elseif ($part === 'files' && preg_match('/-{5,}/', $line)) { + $part = 'total'; + continue; + } + + if ($part === 'header') { + $parts = explode('=', $line, 2); + if (count($parts) === 2) { + $info[strtolower(trim($parts[0]))] = trim($parts[1]); + } + } elseif ($part === 'files') { + $parts = preg_split('/\s+/', $line, 6); + + if (count($parts) >= 6) { + $files[] = [ + 'date' => trim($parts[0]), + 'time' => trim($parts[1]), + 'attr' => trim($parts[2]), + 'size' => (int)$parts[3], + 'compressed' => (int)$parts[4], + 'path' => trim($parts[5]), + ]; + } + } elseif ($part === 'total') { + $info['total']['raw'] = $line; + $parts = preg_split('/\s+/', $line, 5); + + $info['total']['date'] = trim($parts[0]); + $info['total']['time'] = trim($parts[1]); + $info['total']['size'] = (int)$parts[2]; + $info['total']['compressed'] = (int)$parts[3]; + $files_and_folders = trim($parts[4]); + $info['total']['files'] = 0; + $info['total']['folders'] = 0; + + if (preg_match('/(\d+)\sfiles/', $files_and_folders, $matches)) { + $info['total']['files'] = (int)$matches[1]; + } + + if (preg_match('/(\d+)\sfolders/', $files_and_folders, $matches)) { + $info['total']['folders'] = (int)$matches[1]; + } + + $part = 'discard'; + } + } + + return [ + 'info' => $info, + 'files' => $files, + ]; + } + + public function shouldDeleteSourceAfterExtract(): bool + { + return $this->deleteSourceAfterExtract; + } + + /** + * Untar extracted tar file after extracted original archive then delete tar file + * + * @param string $tarFile + * @param array $extractCommand + * @return string + */ + public function executeUntarAfter(string $tarFile, array $extractCommand): string + { + $sourceTar = $this->getTargetPath() . '/' . $tarFile; + + $sz = new self(); + $sz + ->format('tar') + ->deleteSourceAfterExtract() + ->setCustomFlags($this->getCustomFlags()) + ->source($sourceTar) + ->target($this->getTargetPath()); + + + if ($this->getProgressCallback() !== null) { + $sz->progress($this->getProgressCallback()); + } + + + // 'snoi' => store owner id in archive, extract owner id from archive (tar/Linux) + // 'snon' => store owner name in archive (tar/Linux) + // 'mtc' => Stores Creation timestamps for files (for pax method). + // 'mta' => Stores last Access timestamps for files (for pax method). + // 'mtm' => Stores last Modification timestamps for files ). + if ($this->shouldKeepFileInfoOnTar()) { + $sz + ->addFlag('snoi') + ->addFlag('snon') + ->addFlag('mtc', 'on') + ->addFlag('mta', 'on') + ->addFlag('mtm', 'on'); + } else { + $sz->addFlag('mtc', 'off')->addFlag('mta', 'off')->addFlag('mtm', 'off'); + } + + $output = $this->runCommand($extractCommand); + $output .= $sz->extract(); + unset($sz); + + return $output; + } + + /** + * Set the target path for compression/extraction using a fluent interface. + * + * @param string|null $path The path to the target file or directory for compression or extraction. + * @return $this The current instance of the SevenZip class. + */ + public function target(?string $path): self + { + return $this->setTargetPath($path); + } + + /** + * Sets the source path for the compression or extraction operation. + * + * @param string $path The source path. + * @return static The current instance of the SevenZip class. + */ + public function source(string $path): self + { + return $this->setSourcePath($path); + } + + public function deleteSourceAfterExtract(bool $delete = true): SevenZip + { + $this->deleteSourceAfterExtract = $delete; + return $this; + } + + /** + * Set the progress callback using a fluent interface. + * + * @param callable $callback The callback function to be called during the compression progress. + * @return $this The current instance of the SevenZip class. + */ + public function progress(callable $callback): self + { + return $this->setProgressCallback($callback); + } + + /** + * Get whether to keep file info when using TAR before compression. + * + * @return bool Whether to keep file info when using TAR before compression. + */ + public function shouldKeepFileInfoOnTar(): bool + { + return $this->keepFileInfoOnTar; + } + + public function autoUntar(bool $auto = true): SevenZip + { + $this->autoUntar = $auto; + return $this; + } + + /** + * Compress a file or directory. + * + * @return string The output of the 7-Zip command. + * @throws \InvalidArgumentException If target path, or source path is not set. + * + */ + public function compress(): string + { + if (!$this->getTargetPath()) { + throw new \InvalidArgumentException( + "Archive file path (target) must be set" + ); + } + + if (!$this->getSourcePath()) { + throw new \InvalidArgumentException( + "File or directory path (source) must be set" + ); + } + + if ($this->getFormat() === "zip") { + if (!$this->getFlag("mm")) { + $this->mm("Deflate64"); + } + + if ($this->getPassword()) { + $this->addFlag("mem", $this->getZipEncryptionMethod()); + } + } + + if ( + $this->getPassword() && + $this->getEncryptNames() && + $this->getFormat() !== "zip" + ) { + $this->addFlag("mhe"); + } + + if ($this->getPassword()) { + $this->addFlag("p", $this->getPassword(), glued: true); + } + + if ($this->shouldForceTarBefore()) { + $this->executeTarBefore(); + } + + $command = [ + $this->sevenZipPath, + "a", + ...$this->flagrize($this->getAlwaysFlags()), + ...$this->flagrize($this->getFormatFlags()), + ...$this->flagrize($this->getCustomFlags()), + $this->getTargetPath(), + $this->getSourcePath(), + ]; + + return $this->runCommand($command); + } + + public function getFlag(string $flag): mixed + { + return $this->customFlags[$flag] ?? null; + } + + /** + * Set the compression method for ZIP format + * + * @param string $method Sets a method: Copy, Deflate, Deflate64, BZip2, LZMA, PPMd. + * @return $this + */ + public function mm(string $method): self + { + return $this->addFlag("mm", $method); + } + + /** + * Get the ZIP encryption method. + * + * @return string The ZIP encryption method. + */ + public function getZipEncryptionMethod(): string + { + return $this->zipEncryptionMethod; + } + + /** + * Set the ZIP encryption method. + * + * @param string $zipEncryptionMethod The ZIP encryption method to be used. + * @return $this The current instance of the SevenZip class. + */ + public function setZipEncryptionMethod(string $zipEncryptionMethod): SevenZip + { + $this->zipEncryptionMethod = $zipEncryptionMethod; + return $this; + } + + public function getEncryptNames(): ?bool + { + return $this->encryptNames; + } + + /** + * Set whether to encrypt file names or not. + * + * @param bool $encrypt Whether or not to encrypt file names. + * @return $this The current instance of the SevenZip class. + */ + public function setEncryptNames(?bool $encrypt): SevenZip + { + $this->encryptNames = $encrypt; + return $this; + } + + /** + * Get whether to force TAR before compression. + * + * @return bool Whether to force TAR before compression. + */ + public function shouldForceTarBefore(): bool + { + return $this->forceTarBefore; + } + + /* + * Sets level of file analysis. + * + * @param int $level + * @return $this + */ + + /** + * Tars the source file or directory before compressing. + * + * @return $this The current instance of the SevenZip class. + */ + protected function executeTarBefore(): self + { + if ($this->wasAlreadyTarred()) { + return $this; + } + + if (!$this->getSourcePath()) { + throw new \InvalidArgumentException( + "File or directory path (source) must be set" + ); + } + + $sourcePath = $this->getSourcePath(); + $tarPath = sys_get_temp_dir() . '/' . uniqid('sevenzip_') . '/' . basename($sourcePath) . '.tar'; + + try { + $sz = new self(); + $sz + ->format("tar") + ->target($tarPath) + ->source($sourcePath); + + if ($this->getProgressCallback() !== null) { + $sz->progress($this->getProgressCallback()); + } + + + // 'snoi' => store owner id in archive, extract owner id from archive (tar/Linux) + // 'snon' => store owner name in archive (tar/Linux) + // 'mtc' => Stores Creation timestamps for files (for pax method). + // 'mta' => Stores last Access timestamps for files (for pax method). + // 'mtm' => Stores last Modification timestamps for files ). + if ($this->shouldKeepFileInfoOnTar()) { + $sz + ->addFlag("snoi") + ->addFlag("snon") + ->addFlag("mtc", "on") + ->addFlag("mta", "on") + ->addFlag("mtm", "on"); + } else { + $sz->addFlag("mtc", "off")->addFlag("mta", "off")->addFlag("mtm", "off"); + } + + $sz->compress(); + unset($sz); + + $this->setAlreadyTarred(true); + + $this->setSourcePath($tarPath)->deleteSourceAfterCompress(); + } + catch (Exception $e) { + unlink($tarPath); + throw $e; + // @TODO use native tar? + } + + return $this; + } + + /** + * Get whether the source is already TARred. + * + * @return bool Whether the source is already TARred. + */ + public function wasAlreadyTarred(): bool + { + return $this->alreadyTarred; + } + + /** + * Set whether the source is already TARred. + * + * @param bool $alreadyTarred Whether the source is already TARred. + * @return $this The current instance of the SevenZip class. + */ + public function setAlreadyTarred(bool $alreadyTarred): SevenZip + { + $this->alreadyTarred = $alreadyTarred; + return $this; + } + + /** + * Configure to delete the source file or directory after compression (alias for sdel) + * + * @return $this + */ + public function deleteSourceAfterCompress(): self + { + return $this->sdel(); + } + + /** + * Configure to delete the source file or directory after compression (same of deleteSourceAfterCompress()) + * @return $this + */ + public function sdel(): self + { + return $this->addFlag("sdel"); + } + + /** + * Get the default compression flags for the specified format. + * + * @return array The default compression flags for the specified format. + */ + protected function getFormatFlags(): array + { + return $this->formatFlags ?? []; + } + + public function setFormatFlags(): SevenZip + { + $this->formatFlags = match ($this->getFormat()) { + 'zip', 'tar.zip' => ['tzip'], + '7z', 'lzma2', 'tar.7z' => ['t7z', 'm0' => 'lzma2'], + 'lz4', 'tar.lz4' => ['t7z', 'm0' => 'lz4'], + 'lz5', 'tar.lz5' => ['t7z', 'm0' => 'lz5'], + 'bz2', 'bzip2', 'tar.bz2', 'tar.bz' => ['t7z', 'm0' => 'bzip2'], + 'zstd', 'zst', => ['t7z', 'm0' => 'zstd'], + 'brotli', 'br' => ['t7z', 'm0' => 'brotli'], + 'gzip', 'gz' => ['t7z', 'm0' => 'gzip'], + 'tar' => [ + 'ttar', + 'mm' => 'pax', // Modern POSIX + 'mtp' => '1', // Sets timestamp precision to 1 second (unix timestamp) + ], + // Down below if not supported will throw an exception + 'tar.zstd', 'tar.zst', 'tzst' => ['tzstd'], + 'tgz', 'tar.gz', 'tgzip' => ['tgzip'], + 'tbr', 'tar.br', 'tar.brotli' => ['tbrotli'], + default => ['t' . $this->getFormat()], + }; + + $needsTar = strpos($this->getFormat(), 't') === 0 && $this->getFormat() !== 'tar'; + + return $needsTar ? $this->tarBefore() : $this; + } + + /** + * Set the compression level to faster. + * + * @return $this The current instance of the SevenZip class. + */ + public function faster(): self + { + if ($this->getFormat() === "zstd" || $this->getFormat() === "zst") { + return $this->mx(0); + } + + return $this->mx(1); + } + + /** + * Set the compression level using the -mx flag. + * + * @param int $level The compression level to be used. + * @return $this The current instance of the SevenZip class. + */ + public function mx(int $level): self + { + return $this->addFlag("mx", $level); + } + + /** + * Set the compression level to slower. + * + * @return $this The current instance of the SevenZip class. + */ + public function slower(): self + { + if ($this->getFormat() === "zstd" || $this->getFormat() === "zst") { + return $this->mx(22); + } + + return $this->mx(9); + } + + /** + * Configures maximum compression settings based on the specified format. + * + * @return static The current instance for method chaining. + */ + public function ultra(): self + { + $this->mmt(true)->mx(9); + + return match ($this->getFormat()) { + "zip" => $this->mm("Deflate64")->mfb(257)->mpass(15)->mmem(28), + "gzip" => $this->mfb(258)->mpass(15), + "bzip2" => $this->mpass(7)->md("900000b"), + "7z" => $this->m0("lzma2")->mfb(64)->ms(true)->md("32m"), + "zstd", "zst" => $this->mx(22), + default => $this, + }; + } + + public function mmem(int|string $size = 24) + { + return $this->addFlag("mmem", $size); + } + + /** + * Set the number of passes for compression. + * + * @param int $number The number of passes for compression. + * @return $this The current instance of the SevenZip class. + */ + public function mpass(int $number = 7): self + { + return $this->addFlag("mpass", $number); + } + + /** + * Set the size of the Fast Bytes for the compression algorithm. + * + * @param int $bytes The size of the Fast Bytes. The default value (when set) is 64. + * @return $this The current instance of the SevenZip class. + */ + public function mfb(int $bytes = 64): self + { + return $this->addFlag("mfb", $bytes); + } + + public function md(string $size = "32m"): self + { + return $this->addFlag("md", $size); + } + + public function ms(bool|string|int $on = true): self + { + return $this->addFlag("ms", $on ? "on" : "off"); + } + + /** + * Set the main/first compression method. + * @param $method string The compression method to be used. + * @return $this + */ + public function m0(string $method): self + { + return $this->addFlag("m0", $method); + } + + /** + * Set the second compression method. + * + * @param $method string The compression method to be used. + * @return $this + */ + public function m1($method): self + { + return $this->addFlag("m1", $method); + } + + /** + * Set the third compression method + * + * @param $method string The compression method to be used. + * @return $this + */ + public function m2(string $method): self + { + return $this->addFlag("m2", $method); + } + + public function solid(bool|string|int $on = true): self + { + return $this->ms($on); + } + + /** + * Configures no compression (copy only) settings based on the specified format. + * + * @return static The current instance for method chaining. + */ + public function copy(): self + { + return $this->mmt(true)->mx(0)->m0("Copy")->mm("Copy")->myx(0); + } + + /** + * Sets file analysis level. + * + * @param int $level + * @return $this + */ + public function myx(int $level = 5): self + { + return $this->addFlag("myx", $level); + } + + /** + * Get the idle timeout value. + * + * @return int The idle timeout value in seconds. + */ + public function getIdleTimeout(): int + { + return $this->idleTimeout; + } + + /** + * Set the idle timeout value. + * + * @param int $idleTimeout The idle timeout value in seconds. + * @return $this The current instance of the SevenZip class. + */ + public function setIdleTimeout(int $idleTimeout): SevenZip + { + $this->idleTimeout = $idleTimeout; + return $this; } } diff --git a/tests/SevenZipTest.php b/tests/SevenZipTest.php index ae7ec69..3b1d807 100644 --- a/tests/SevenZipTest.php +++ b/tests/SevenZipTest.php @@ -84,6 +84,12 @@ public function testFlagrize(): void $this->assertEquals($expected, $result); } + /** + * @covers \Verseles\SevenZip\SevenZip::getAlwaysFlags + * @covers \Verseles\SevenZip\SevenZip::setAlwaysFlags + * @return void + * @throws \ReflectionException + */ public function testGetAndSetAlwaysFlags(): void { $getAlwaysFlags = $this->getProtectedMethod($this->sevenZip, 'getAlwaysFlags'); @@ -106,25 +112,54 @@ private static function getProtectedMethod($object, $methodName) return $method; } + /** + * @covers \Verseles\SevenZip\SevenZip::reset + */ public function testReset(): void { - $this->sevenZip->addFlag('custom', 'value'); - $this->sevenZip->setProgressCallback(function () { }); - $this->sevenZip->setFormat('zip'); - $this->sevenZip->setTargetPath('/path/to/target'); - $this->sevenZip->setSourcePath('/path/to/source'); - $this->sevenZip->encrypt('my_secret_password'); + $this->sevenZip + ->addFlag('mmt', 'on') + ->addFlag('mx', '9') + ->setProgressCallback(function () { + }) + ->setFormat('zip') + ->setTargetPath('/path/to/target') + ->setSourcePath('/path/to/source') + ->setPassword('password') + ->setEncryptNames(false) + ->setTimeout(600) + ->setIdleTimeout(240) + ->setZipEncryptionMethod('AES128') + ->forceTarBefore(true) + ->keepFileInfoOnTar(false) + ->setAlreadyTarred(true) + ->autoUntar(false) + ->deleteSourceAfterExtract(true); $this->sevenZip->reset(); $this->assertEmpty($this->sevenZip->getCustomFlags()); $this->assertNull($this->sevenZip->getProgressCallback()); - $this->assertEquals('7z', $this->sevenZip->getFormat()); - $this->assertEmpty($this->sevenZip->getTargetPath()); - $this->assertEmpty($this->sevenZip->getSourcePath()); + $this->assertSame(-1, $this->sevenZip->getLastProgress()); + $this->assertSame('7z', $this->sevenZip->getFormat()); + $this->assertNull($this->sevenZip->getTargetPath()); + $this->assertNull($this->sevenZip->getSourcePath()); $this->assertNull($this->sevenZip->getPassword()); + $this->assertTrue($this->sevenZip->getEncryptNames()); + $this->assertSame(300, $this->sevenZip->getTimeout()); + $this->assertSame(120, $this->sevenZip->getIdleTimeout()); + $this->assertSame('AES256', $this->sevenZip->getZipEncryptionMethod()); + $this->assertFalse($this->sevenZip->shouldForceTarBefore()); + $this->assertTrue($this->sevenZip->shouldKeepFileInfoOnTar()); + $this->assertFalse($this->sevenZip->wasAlreadyTarred()); + $this->assertTrue($this->sevenZip->shouldAutoUntar()); + $this->assertFalse($this->sevenZip->shouldDeleteSourceAfterExtract()); } + /** + * @covers \Verseles\SevenZip\SevenZip::progress + * @return void + */ public function testProgress(): void { $callback = function () { }; @@ -132,7 +167,11 @@ public function testProgress(): void $this->assertEquals($callback, $this->sevenZip->getProgressCallback()); } - public function testFasterAndSlower(): void + /** + * @covers \Verseles\SevenZip\SevenZip::faster + * @covers \Verseles\SevenZip\SevenZip::slower + */ + public function testCompressionLevels(): void { $this->sevenZip->format('zstd'); $this->sevenZip->faster(); @@ -149,6 +188,9 @@ public function testFasterAndSlower(): void $this->assertEquals(['mx' => 9, 'mmt' => 'on'], $this->sevenZip->getCustomFlags()); } + /** + * @covers \Verseles\SevenZip\SevenZip::ultra + */ public function testUltraZip(): void { $this->sevenZip->format('zip'); @@ -164,6 +206,9 @@ public function testUltraZip(): void $this->assertEquals($expected, $this->sevenZip->getCustomFlags()); } + /** + * @covers \Verseles\SevenZip\SevenZip::ultra + */ public function testUltraZstd(): void { @@ -176,6 +221,9 @@ public function testUltraZstd(): void $this->assertEquals($expected, $this->sevenZip->getCustomFlags()); } + /** + * @covers \Verseles\SevenZip\SevenZip::ultra + */ public function testUltra7z(): void { $this->sevenZip->format('7z'); @@ -191,6 +239,9 @@ public function testUltra7z(): void $this->assertEquals($expected, $this->sevenZip->getCustomFlags()); } + /** + * @covers \Verseles\SevenZip\SevenZip::copy + */ public function testCopy(): void { $this->sevenZip->copy(); @@ -206,6 +257,7 @@ public function testCopy(): void /** * @dataProvider compressAndExtractDataProvider + * @covers \Verseles\SevenZip\SevenZip::compress */ public function testCompress(string $format): void { @@ -225,6 +277,7 @@ public function testCompress(string $format): void /** * @dataProvider compressAndExtractDataProvider + * @covers \Verseles\SevenZip\SevenZip::extract * @depends testCompress */ public function testExtract(string $format): void @@ -243,7 +296,11 @@ public function testExtract(string $format): void unlink($archive); } - public function testAddAndRemoveFlag(): void + /** + * @covers \Verseles\SevenZip\SevenZip::addFlag + * @covers \Verseles\SevenZip\SevenZip::removeFlag + */ + public function testFlagManagement(): void { $flag = 'mx'; $value = 9; @@ -279,19 +336,10 @@ public function testSetProgressCallback(): void $this->assertEquals(100, end($progressHistory)); } - public function testGetDefaultCompressFlags(): void - { - $defaultFlags = self::getProtectedMethod($this->sevenZip, 'getDefaultCompressFlags'); - - $this->assertEquals(['tzip'], $defaultFlags->invoke($this->sevenZip, 'zip')); - $this->assertEquals(['t7z', 'm0' => 'lzma2'], $defaultFlags->invoke($this->sevenZip, '7z')); - $this->assertEquals(['t7z', 'm0' => 'bzip2'], $defaultFlags->invoke($this->sevenZip, 'bzip2')); - $this->assertEquals(['tmy_format'], $defaultFlags->invoke($this->sevenZip, 'my_format')); - } - /** * Test encrypting and decrypting a file. - * + * @covers \Verseles\SevenZip\SevenZip::encrypt + * @covers \Verseles\SevenZip\SevenZip::decrypt * @return void */ public function testEncryptAndDecrypt(): void @@ -443,4 +491,109 @@ public function testInclude(): void $this->assertFileDoesNotExist($extractPath . '/js_interop_usage.md'); $this->assertFileDoesNotExist($extractPath . '/js-types.md'); } + + public function testTarBeforeImplicit(): void + { + $tarPath = $this->testDir . '/target/archive.7z'; + $this->sevenZip + ->format('tar.7z') + ->faster() + ->source($this->testDir . '/source/*') + ->target($tarPath) + ->compress(); + + + // Assert that the tar file exists + $this->assertFileExists($tarPath); + } + + /** + * @covers \Verseles\SevenZip\SevenZip::tarBefore + * @covers \Verseles\SevenZip\SevenZip::fileInfo + * @covers \Verseles\SevenZip\SevenZip::fileList + * @return void + */ + public function testTarBeforeExplicit(): string + { + $tarPath = $this->testDir . '/target/archive.tar.7z'; + $this->sevenZip + ->format('7z') + ->faster() + ->source($this->testDir . '/source/*') + ->target($tarPath) + ->tarBefore() + ->compress(); + + + $this->assertFileExists($tarPath); + + $sz = $this->sevenZip->source($tarPath); + + $fileInfo = $sz->fileInfo(); + $fileList = $sz->fileList(); + + $this->assertIsArray($fileInfo); + $this->assertCount(1, $fileList); + + return $tarPath; + } + + /** + * @depends testTarBeforeExplicit + * @covers \Verseles\SevenZip\SevenZip::autoUntar + * @covers \Verseles\SevenZip\SevenZip::shouldAutoUntar + * @return void + */ + public function testAutoUntar($tarPath) + { + $this->assertFileExists($tarPath); + $extractPath = $this->testDir . '/extract/auto_untar/'; + $this->sevenZip + ->source($tarPath) + ->target($extractPath) + ->extract(); + + $this->assertFileExists($extractPath . '/Avatart.svg'); + } + + /** + * @depends testTarBeforeExplicit + * @covers \Verseles\SevenZip\SevenZip::autoUntar + * @covers \Verseles\SevenZip\SevenZip::shouldAutoUntar + * @return void + */ + public function testNotAutoUntar($tarPath) + { + $this->assertFileExists($tarPath); + $extractPath = $this->testDir . '/extract/not_auto_untar/'; + $this->sevenZip + ->source($tarPath) + ->target($extractPath) + ->autoUntar(false) + ->extract(); + + $this->assertFileDoesNotExist($extractPath . '/Avatart.svg'); + } + + /** + * @depends testTarBeforeExplicit + * @covers \Verseles\SevenZip\SevenZip::deleteSourceAfterExtract + * @covers \Verseles\SevenZip\SevenZip::shouldDeleteSourceAfterExtract + * @return void + */ + public function testDeleteSourceAfterExtract($tarPath) + { + $this->assertFileExists($tarPath); + $extractPath = $this->testDir . '/extract/delete_source/'; + $this->sevenZip + ->source($tarPath) + ->deleteSourceAfterExtract() + ->target($extractPath) + ->extract(); + + $this->assertFileDoesNotExist($tarPath); + + } + + } diff --git a/try.php b/try.php index 496d214..36b76f6 100644 --- a/try.php +++ b/try.php @@ -5,40 +5,60 @@ use Verseles\SevenZip\SevenZip; -$archivePath = '/Users/helio/zip-big.7z'; +$format = 'tar.7z'; +$archivePath = "/Users/helio/zip.$format"; $extractPath = '/Users/helio/tmp'; -$fileToCompress = '/Users/helio/zip-big'; +$fileToCompress = '/Users/helio/zip-tiny'; $password = 'test2'; -@unlink($archivePath); -@unlink($extractPath . '/' . $fileToCompress); +unlink($archivePath); +unlink($extractPath . '/' . $fileToCompress); echo "Creating instance of SevenZip... "; $sevenZip = new SevenZip(); echo "✅\n"; echo 'Compressing archive... '; -$sevenZip - ->setProgressCallback(function ($progress) { - echo "\n" . $progress . "%\n"; - }) - ->format('zstd') - ->encrypt($password) +$callback = function ($progress) { + echo "\n" . $progress . "%\n"; +}; +$output = $sevenZip + ->progress($callback) + ->format($format) +// ->encrypt($password) ->source($fileToCompress) ->target($archivePath) - ->compress(); + ->exclude('*.git/*') +// ->solid(true) +// ->setTimeout(10) +// ->ultra() + ->faster() +// ->copy() +->compress(); echo "✅\n"; -// -//echo 'Extracting archive... '; -//$sevenZip -// ->setProgressCallback(function ($progress) { -// echo "\n" . $progress . "%\n"; -// }) +echo "Output: " . $output . "\n"; + +//echo "Info archive... "; +//$output = $sevenZip // ->source($archivePath) -// ->target($extractPath) -// ->decrypt($password) -// ->extract(); +//// ->decrypt($password) +// ->fileInfo(); +// //echo "✅\n"; +//echo "Output: \n"; + +echo 'Extracting archive... '; +$output = $sevenZip + ->setProgressCallback(function ($progress) { + echo "\n" . $progress . "%\n"; + }) +// ->autoUntar(false) + ->source($archivePath) + ->target($extractPath) +// ->decrypt($password) + ->extract(); +echo "✅\n"; +echo "Output: " . $output . "\n"; // //echo "Deleting archive... "; //unlink($archivePath);