Added new command to diff two backup files and find the difference

This commit is contained in:
ArabCoders
2025-01-30 18:27:43 +03:00
parent 96ccd88cd0
commit 826b0f9508
4 changed files with 758 additions and 598 deletions

1
.gitignore vendored
View File

@@ -4,3 +4,4 @@
/.phpunit.result.cache
/.vscode
/frontend/exported/
.phpactor.json

107
composer.lock generated
View File

@@ -195,25 +195,25 @@
},
{
"name": "laravel/serializable-closure",
"version": "v2.0.1",
"version": "v2.0.2",
"source": {
"type": "git",
"url": "https://github.com/laravel/serializable-closure.git",
"reference": "613b2d4998f85564d40497e05e89cb6d9bd1cbe8"
"reference": "2e1a362527783bcab6c316aad51bf36c5513ae44"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/serializable-closure/zipball/613b2d4998f85564d40497e05e89cb6d9bd1cbe8",
"reference": "613b2d4998f85564d40497e05e89cb6d9bd1cbe8",
"url": "https://api.github.com/repos/laravel/serializable-closure/zipball/2e1a362527783bcab6c316aad51bf36c5513ae44",
"reference": "2e1a362527783bcab6c316aad51bf36c5513ae44",
"shasum": ""
},
"require": {
"php": "^8.1"
},
"require-dev": {
"illuminate/support": "^10.0|^11.0",
"illuminate/support": "^10.0|^11.0|^12.0",
"nesbot/carbon": "^2.67|^3.0",
"pestphp/pest": "^2.36",
"pestphp/pest": "^2.36|^3.0",
"phpstan/phpstan": "^2.0",
"symfony/var-dumper": "^6.2.0|^7.0.0"
},
@@ -252,7 +252,7 @@
"issues": "https://github.com/laravel/serializable-closure/issues",
"source": "https://github.com/laravel/serializable-closure"
},
"time": "2024-12-16T15:26:28+00:00"
"time": "2025-01-24T15:42:37+00:00"
},
{
"name": "league/container",
@@ -1569,16 +1569,16 @@
},
{
"name": "symfony/cache",
"version": "v7.2.1",
"version": "v7.2.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/cache.git",
"reference": "e7e983596b744c4539f31e79b0350a6cf5878a20"
"reference": "8d773a575e446de220dca03d600b2d8e1c1c10ec"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/cache/zipball/e7e983596b744c4539f31e79b0350a6cf5878a20",
"reference": "e7e983596b744c4539f31e79b0350a6cf5878a20",
"url": "https://api.github.com/repos/symfony/cache/zipball/8d773a575e446de220dca03d600b2d8e1c1c10ec",
"reference": "8d773a575e446de220dca03d600b2d8e1c1c10ec",
"shasum": ""
},
"require": {
@@ -1647,7 +1647,7 @@
"psr6"
],
"support": {
"source": "https://github.com/symfony/cache/tree/v7.2.1"
"source": "https://github.com/symfony/cache/tree/v7.2.3"
},
"funding": [
{
@@ -1663,7 +1663,7 @@
"type": "tidelift"
}
],
"time": "2024-12-07T08:08:50+00:00"
"time": "2025-01-27T11:08:17+00:00"
},
{
"name": "symfony/cache-contracts",
@@ -2059,16 +2059,16 @@
},
{
"name": "symfony/http-client",
"version": "v7.2.2",
"version": "v7.2.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-client.git",
"reference": "339ba21476eb184290361542f732ad12c97591ec"
"reference": "7ce6078c79a4a7afff931c413d2959d3bffbfb8d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-client/zipball/339ba21476eb184290361542f732ad12c97591ec",
"reference": "339ba21476eb184290361542f732ad12c97591ec",
"url": "https://api.github.com/repos/symfony/http-client/zipball/7ce6078c79a4a7afff931c413d2959d3bffbfb8d",
"reference": "7ce6078c79a4a7afff931c413d2959d3bffbfb8d",
"shasum": ""
},
"require": {
@@ -2134,7 +2134,7 @@
"http"
],
"support": {
"source": "https://github.com/symfony/http-client/tree/v7.2.2"
"source": "https://github.com/symfony/http-client/tree/v7.2.3"
},
"funding": [
{
@@ -2150,7 +2150,7 @@
"type": "tidelift"
}
],
"time": "2024-12-30T18:35:15+00:00"
"time": "2025-01-28T15:51:35+00:00"
},
{
"name": "symfony/http-client-contracts",
@@ -2232,16 +2232,16 @@
},
{
"name": "symfony/lock",
"version": "v7.2.0",
"version": "v7.2.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/lock.git",
"reference": "07212a5994a30e3667e95e5b16b2dda0685aff84"
"reference": "4f6e8b0e03e4a76095f7d058d72e72d30d5f59e5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/lock/zipball/07212a5994a30e3667e95e5b16b2dda0685aff84",
"reference": "07212a5994a30e3667e95e5b16b2dda0685aff84",
"url": "https://api.github.com/repos/symfony/lock/zipball/4f6e8b0e03e4a76095f7d058d72e72d30d5f59e5",
"reference": "4f6e8b0e03e4a76095f7d058d72e72d30d5f59e5",
"shasum": ""
},
"require": {
@@ -2290,7 +2290,7 @@
"semaphore"
],
"support": {
"source": "https://github.com/symfony/lock/tree/v7.2.0"
"source": "https://github.com/symfony/lock/tree/v7.2.3"
},
"funding": [
{
@@ -2306,7 +2306,7 @@
"type": "tidelift"
}
],
"time": "2024-10-25T15:34:29+00:00"
"time": "2025-01-17T06:59:03+00:00"
},
{
"name": "symfony/process",
@@ -2541,16 +2541,16 @@
},
{
"name": "symfony/var-dumper",
"version": "v7.2.0",
"version": "v7.2.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/var-dumper.git",
"reference": "c6a22929407dec8765d6e2b6ff85b800b245879c"
"reference": "82b478c69745d8878eb60f9a049a4d584996f73a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/c6a22929407dec8765d6e2b6ff85b800b245879c",
"reference": "c6a22929407dec8765d6e2b6ff85b800b245879c",
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/82b478c69745d8878eb60f9a049a4d584996f73a",
"reference": "82b478c69745d8878eb60f9a049a4d584996f73a",
"shasum": ""
},
"require": {
@@ -2604,7 +2604,7 @@
"dump"
],
"support": {
"source": "https://github.com/symfony/var-dumper/tree/v7.2.0"
"source": "https://github.com/symfony/var-dumper/tree/v7.2.3"
},
"funding": [
{
@@ -2620,7 +2620,7 @@
"type": "tidelift"
}
],
"time": "2024-11-08T15:48:14+00:00"
"time": "2025-01-17T11:39:41+00:00"
},
{
"name": "symfony/var-exporter",
@@ -2700,16 +2700,16 @@
},
{
"name": "symfony/yaml",
"version": "v7.2.0",
"version": "v7.2.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/yaml.git",
"reference": "099581e99f557e9f16b43c5916c26380b54abb22"
"reference": "ac238f173df0c9c1120f862d0f599e17535a87ec"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/yaml/zipball/099581e99f557e9f16b43c5916c26380b54abb22",
"reference": "099581e99f557e9f16b43c5916c26380b54abb22",
"url": "https://api.github.com/repos/symfony/yaml/zipball/ac238f173df0c9c1120f862d0f599e17535a87ec",
"reference": "ac238f173df0c9c1120f862d0f599e17535a87ec",
"shasum": ""
},
"require": {
@@ -2752,7 +2752,7 @@
"description": "Loads and dumps YAML files",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/yaml/tree/v7.2.0"
"source": "https://github.com/symfony/yaml/tree/v7.2.3"
},
"funding": [
{
@@ -2768,7 +2768,7 @@
"type": "tidelift"
}
],
"time": "2024-10-23T06:56:12+00:00"
"time": "2025-01-07T12:55:42+00:00"
},
{
"name": "webmozart/assert",
@@ -3453,16 +3453,16 @@
},
{
"name": "phpunit/phpunit",
"version": "11.5.3",
"version": "11.5.5",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "30e319e578a7b5da3543073e30002bf82042f701"
"reference": "b9a975972f580c0491f834eb0818ad2b32fd8bba"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/30e319e578a7b5da3543073e30002bf82042f701",
"reference": "30e319e578a7b5da3543073e30002bf82042f701",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b9a975972f580c0491f834eb0818ad2b32fd8bba",
"reference": "b9a975972f580c0491f834eb0818ad2b32fd8bba",
"shasum": ""
},
"require": {
@@ -3534,7 +3534,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
"source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.3"
"source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.5"
},
"funding": [
{
@@ -3550,7 +3550,7 @@
"type": "tidelift"
}
],
"time": "2025-01-13T09:36:00+00:00"
"time": "2025-01-29T14:01:11+00:00"
},
{
"name": "psalm/phar",
@@ -3593,12 +3593,12 @@
"source": {
"type": "git",
"url": "https://github.com/Roave/SecurityAdvisories.git",
"reference": "fb6b00411f2c212631318ab412b2208632e507ba"
"reference": "a39f409dd81c4cde087ab72b9026c013f09fc492"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/fb6b00411f2c212631318ab412b2208632e507ba",
"reference": "fb6b00411f2c212631318ab412b2208632e507ba",
"url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/a39f409dd81c4cde087ab72b9026c013f09fc492",
"reference": "a39f409dd81c4cde087ab72b9026c013f09fc492",
"shasum": ""
},
"conflict": {
@@ -3683,7 +3683,7 @@
"cart2quote/module-quotation-encoded": ">=4.1.6,<=4.4.5|>=5,<5.4.4",
"cartalyst/sentry": "<=2.1.6",
"catfan/medoo": "<1.7.5",
"causal/oidc": "<2.1",
"causal/oidc": "<4",
"cecil/cecil": "<7.47.1",
"centreon/centreon": "<22.10.15",
"cesnet/simplesamlphp-module-proxystatistics": "<3.1",
@@ -3736,7 +3736,7 @@
"doctrine/mongodb-odm": "<1.0.2",
"doctrine/mongodb-odm-bundle": "<3.0.1",
"doctrine/orm": ">=1,<1.2.4|>=2,<2.4.8|>=2.5,<2.5.1|>=2.8.3,<2.8.4",
"dolibarr/dolibarr": "<19.0.2",
"dolibarr/dolibarr": "<19.0.2|==21.0.0.0-beta",
"dompdf/dompdf": "<2.0.4",
"doublethreedigital/guest-entries": "<3.1.2",
"drupal/core": ">=6,<6.38|>=7,<7.102|>=8,<10.2.11|>=10.3,<10.3.9|>=11,<11.0.8",
@@ -4071,7 +4071,7 @@
"phpfastcache/phpfastcache": "<6.1.5|>=7,<7.1.2|>=8,<8.0.7",
"phpmailer/phpmailer": "<6.5",
"phpmussel/phpmussel": ">=1,<1.6",
"phpmyadmin/phpmyadmin": "<5.2.1",
"phpmyadmin/phpmyadmin": "<5.2.2",
"phpmyfaq/phpmyfaq": "<3.2.5|==3.2.5|>=3.2.10,<=4.0.1",
"phpoffice/common": "<0.2.9",
"phpoffice/phpexcel": "<1.8.1",
@@ -4085,13 +4085,13 @@
"phpxmlrpc/phpxmlrpc": "<4.9.2",
"pi/pi": "<=2.5",
"pimcore/admin-ui-classic-bundle": "<1.5.4",
"pimcore/customer-management-framework-bundle": "<4.0.6",
"pimcore/customer-management-framework-bundle": "<4.2.1",
"pimcore/data-hub": "<1.2.4",
"pimcore/data-importer": "<1.8.9|>=1.9,<1.9.3",
"pimcore/demo": "<10.3",
"pimcore/ecommerce-framework-bundle": "<1.0.10",
"pimcore/perspective-editor": "<1.5.1",
"pimcore/pimcore": "<11.2.4",
"pimcore/pimcore": "<11.2.4|>=11.4.2,<11.5.3",
"pixelfed/pixelfed": "<0.11.11",
"plotly/plotly.js": "<2.25.2",
"pocketmine/bedrock-protocol": "<8.0.2",
@@ -4105,6 +4105,7 @@
"prestashop/gamification": "<2.3.2",
"prestashop/prestashop": "<8.1.6",
"prestashop/productcomments": "<5.0.2",
"prestashop/ps_contactinfo": "<=3.3.2",
"prestashop/ps_emailsubscription": "<2.6.1",
"prestashop/ps_facetedsearch": "<3.4.1",
"prestashop/ps_linklist": "<3.1",
@@ -4290,7 +4291,7 @@
"truckersmp/phpwhois": "<=4.3.1",
"ttskch/pagination-service-provider": "<1",
"twbs/bootstrap": "<=3.4.1|>=4,<=4.6.2",
"twig/twig": "<3.11.2|>=3.12,<3.14.1",
"twig/twig": "<3.11.2|>=3.12,<3.14.1|>=3.16,<3.19",
"typo3/cms": "<9.5.29|>=10,<10.4.35|>=11,<11.5.23|>=12,<12.2",
"typo3/cms-backend": "<4.1.14|>=4.2,<4.2.15|>=4.3,<4.3.7|>=4.4,<4.4.4|>=7,<=7.6.50|>=8,<=8.7.39|>=9,<=9.5.24|>=10,<10.4.46|>=11,<11.5.40|>=12,<12.4.21|>=13,<13.3.1",
"typo3/cms-belog": ">=10,<=10.4.47|>=11,<=11.5.41|>=12,<=12.4.24|>=13,<=13.4.2",
@@ -4454,7 +4455,7 @@
"type": "tidelift"
}
],
"time": "2025-01-21T22:04:49+00:00"
"time": "2025-01-29T19:04:15+00:00"
},
{
"name": "sebastian/cli-parser",

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,309 @@
<?php
declare(strict_types=1);
namespace App\Commands\System;
use App\Command;
use App\Libs\Attributes\Route\Cli;
use App\Libs\Entity\StateInterface as iState;
use App\Libs\Mappers\Import\RestoreMapper;
use App\Libs\Stream;
use DirectoryIterator;
use JsonMachine\Exception\InvalidArgumentException;
use Psr\Log\LoggerInterface as iLogger;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface as iInput;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface as iOutput;
/**
* Class DiffCommand
*
* This command is used to compare 2 backup files for difference.
*/
#[Cli(command: self::ROUTE)]
final class DiffCommand extends Command
{
public const string ROUTE = 'system:diff';
private array $contentType = ['all', 'played', 'unplayed'];
private array $sourceType = ['a', 'b'];
/**
* Class Constructor.
*/
public function __construct(private iLogger $logger)
{
set_time_limit(0);
ini_set('memory_limit', '-1');
parent::__construct();
}
/**
* Configure the Tinker command.
*
* @return void
*/
protected function configure(): void
{
$this->setName(self::ROUTE)
->addOption('save', 's', InputOption::VALUE_REQUIRED, 'Save difference in a file.')
->addOption('content', 'c', InputOption::VALUE_REQUIRED, 'Save mode, can be all, played, unplayed.', 'all')
->addOption('source', 'S', InputOption::VALUE_REQUIRED, 'Source of truth, can be a or b.', 'a')
->addArgument(
'files',
InputArgument::IS_ARRAY,
'The files to compare, the first is the original and the second is the new file to compare.'
)
->setDescription('Compare 2 backup files for difference');
}
/**
* Run the interactive shell.
*
* @param iInput $input The input object containing the command input.
* @param iOutput $output The output object for writing command output.
*
* @return int Returns 0 on success or an error code on failure.
* @throws InvalidArgumentException
*/
protected function execute(iInput $input, iOutput $output): int
{
$files = $input->getArgument('files');
$fp = null;
if (null !== ($save = $input->getOption('save'))) {
$fp = new Stream($save, 'wb+');
$fp->write('[');
}
if (false === in_array($input->getOption('content'), $this->contentType, true)) {
$output->writeln('<error>Invalid content type provided. Please provide a valid content type.</error>');
return self::FAILURE;
}
if (false === in_array($input->getOption('source'), $this->sourceType, true)) {
$output->writeln('<error>Invalid source type provided. Please provide a valid source type.</error>');
return self::FAILURE;
}
if (2 !== count($files)) {
$output->writeln('<error>Invalid number of files provided. Please provide 2 files to compare.</error>');
return self::FAILURE;
}
$exists = true;
foreach ($files as $file) {
if (!file_exists($file)) {
$output->writeln("<error>File not found: {$file}</error>");
$exists = false;
}
}
if (false === $exists) {
return self::FAILURE;
}
$mapper1 = $mapper2 = null;
foreach ($files as $file) {
$mapper = new RestoreMapper($this->logger, $file);
if (null === $mapper1) {
$mapper1 = $mapper;
} else {
$mapper2 = $mapper;
}
$time = microtime(true);
$this->logger->info("Loading '{file}' into memory.", ['file' => $file]);
$mapper->loadData();
$end = microtime(true);
$this->logger->info("Finished parsing data from '{file}' in '{time}s'.", [
'file' => $file,
'time' => round($end - $time, 2),
]);
}
$this->logger->notice("Comparing '{memory}' of data. Please wait.", ['memory' => getMemoryUsage()]);
$data = [
'changed' => [],
'not_in_a' => [],
'not_in_b' => [],
];
foreach ($mapper1->getObjects() as $entity) {
if (null === ($entity2 = $mapper2->get($entity))) {
$data['not_in_b'][] = [
'title' => $entity->getName(),
'status' => $entity->isWatched(),
];
continue;
}
if ($entity2->isWatched() !== $entity->isWatched()) {
$data['changed'][] = [
'title' => $entity->getName(),
'a' => $entity->isWatched(),
'b' => $entity2->isWatched(),
'entity_a' => $entity,
'entity_b' => $entity2,
];
}
}
foreach ($mapper2->getObjects() as $entity) {
if (null === $mapper1->get($entity)) {
$data['not_in_a'][] = [
'title' => $entity->getName(),
'status' => $entity->isWatched(),
];
}
}
if (null !== $fp && count($data['changed']) > 0) {
$this->saveContent($input, $data['changed'], $fp);
$fp->close();
}
if ('table' === $input->getOption('output')) {
$newData = [];
foreach (ag($data, 'changed', []) as $row) {
$newData[] = [
'Title' => $row['title'],
'[O] Played' => $row['a'] ? 'Yes' : 'No',
'[N] Played' => $row['b'] ? 'Yes' : 'No',
];
}
$data = $newData;
}
$this->displayContent($data, $output, $input->getOption('output'));
return self::SUCCESS;
}
private function saveContent(iInput $input, array $data, Stream $fp): void
{
$source = $input->getOption('source');
$contentType = $input->getOption('content');
$this->logger->notice("Saving the difference 'Source: {source}, Content: {content}' to '{file}'.", [
'file' => $fp->getMetadata('uri'),
'source' => $source,
'content' => $contentType,
]);
foreach ($data as $row) {
$entity = $row['a' === $source ? 'entity_a' : 'entity_b'];
assert($entity instanceof iState);
if ('played' === $contentType && false === $entity->isWatched()) {
continue;
}
if ('unplayed' === $contentType && true === $entity->isWatched()) {
continue;
}
$fp->write(PHP_EOL . $this->processEntity($entity) . ',');
}
$fp->seek(-1, SEEK_END);
$fp->write(PHP_EOL . ']');
}
private function processEntity(iState $entity): string
{
$arr = [
iState::COLUMN_TYPE => $entity->type,
iState::COLUMN_WATCHED => (int)$entity->isWatched(),
iState::COLUMN_UPDATED => makeDate($entity->updated)->getTimestamp(),
iState::COLUMN_META_SHOW => '',
iState::COLUMN_TITLE => trim($entity->title),
];
if ($entity->isEpisode()) {
$arr[iState::COLUMN_META_SHOW] = trim($entity->title);
$arr[iState::COLUMN_TITLE] = trim(
ag(
$entity->getMetadata($entity->via),
iState::COLUMN_META_DATA_EXTRA . '.' .
iState::COLUMN_META_DATA_EXTRA_TITLE,
$entity->season . 'x' . $entity->episode,
)
);
$arr[iState::COLUMN_SEASON] = $entity->season;
$arr[iState::COLUMN_EPISODE] = $entity->episode;
} else {
unset($arr[iState::COLUMN_META_SHOW]);
}
$arr[iState::COLUMN_YEAR] = $entity->year;
$arr[iState::COLUMN_GUIDS] = array_filter(
$entity->getGuids(),
fn($key) => str_contains($key, 'guid_'),
ARRAY_FILTER_USE_KEY
);
if ($entity->isEpisode()) {
$arr[iState::COLUMN_PARENT] = array_filter(
$entity->getParentGuids(),
fn($key) => str_contains($key, 'guid_'),
ARRAY_FILTER_USE_KEY
);
}
if ($entity->hasPlayProgress()) {
$arr[iState::COLUMN_META_DATA_PROGRESS] = $entity->getPlayProgress();
}
return json_encode($arr, JSON_INVALID_UTF8_IGNORE | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
}
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
if ($input->mustSuggestArgumentValuesFor('files')) {
$realValue = afterLast($input->getCompletionValue(), '/');
$filePath = $input->getCompletionValue();
$dirPath = getcwd();
if (!empty($filePath)) {
if (str_starts_with($filePath, '.')) {
$filePath = getcwd() . DIRECTORY_SEPARATOR . $filePath;
}
$dirPath = $filePath;
if (false === is_dir($dirPath)) {
$dirPath = dirname($dirPath);
}
$dirPath = realpath($dirPath);
}
$suggest = [];
foreach (new DirectoryIterator($dirPath) as $name) {
if ($name->isDot()) {
continue;
}
if (empty($realValue) || true === str_starts_with($name->getFilename(), $realValue)) {
$suggest[] = $dirPath . DIRECTORY_SEPARATOR . $name->getFilename();
}
}
$suggestions->suggestValues($suggest);
}
parent::complete($input, $suggestions);
}
}