You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

177 lines
7.3 KiB

12 months ago
4 years ago
4 years ago
12 months ago
12 months ago
3 years ago
12 months ago
12 months ago
3 years ago
12 months ago
12 months ago
12 months ago
12 months ago
12 months ago
12 months ago
12 months ago
12 months ago
12 months ago
12 months ago
  1. <?php
  2. /*
  3. * Import.php
  4. * Copyright (c) 2021 james@firefly-iii.org
  5. *
  6. * This file is part of the Firefly III Data Importer
  7. * (https://github.com/firefly-iii/data-importer).
  8. *
  9. * This program is free software: you can redistribute it and/or modify
  10. * it under the terms of the GNU Affero General Public License as
  11. * published by the Free Software Foundation, either version 3 of the
  12. * License, or (at your option) any later version.
  13. *
  14. * This program is distributed in the hope that it will be useful,
  15. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17. * GNU Affero General Public License for more details.
  18. *
  19. * You should have received a copy of the GNU Affero General Public License
  20. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  21. */
  22. declare(strict_types=1);
  23. namespace App\Console\Commands;
  24. use App\Console\AutoImports;
  25. use App\Console\HaveAccess;
  26. use App\Console\VerifyJSON;
  27. use App\Enums\ExitCode;
  28. use App\Events\ImportedTransactions;
  29. use App\Exceptions\ImporterErrorException;
  30. use App\Services\Shared\Configuration\Configuration;
  31. use Illuminate\Console\Command;
  32. /**
  33. * Class Import
  34. */
  35. final class Import extends Command
  36. {
  37. use AutoImports;
  38. use HaveAccess;
  39. use VerifyJSON;
  40. protected $description = 'Import into Firefly III. Requires a configuration file and optionally a configuration file.';
  41. protected $signature = 'importer:import
  42. {config : The configuration file. }
  43. {file? : Optionally, the importable file you want to import}
  44. ';
  45. /**
  46. * Execute the console command.
  47. *
  48. * @throws ImporterErrorException
  49. */
  50. public function handle(): int
  51. {
  52. $access = $this->haveAccess();
  53. if (false === $access) {
  54. $this->error(sprintf('No access granted, or no connection is possible to your local Firefly III instance at %s.', config('importer.url')));
  55. app('log')->error(sprintf('Exit code is %s.', ExitCode::NO_CONNECTION->name));
  56. return ExitCode::NO_CONNECTION->value;
  57. }
  58. $this->info(sprintf('Welcome to the Firefly III data importer, v%s', config('importer.version')));
  59. app('log')->debug(sprintf('Now in %s', __METHOD__));
  60. $file = (string) $this->argument('file');
  61. $config = (string) $this->argument('config'); // @phpstan-ignore-line
  62. // validate config path:
  63. if ('' !== $config) {
  64. $directory = dirname($config);
  65. if (!$this->isAllowedPath($directory)) {
  66. $this->error(sprintf('Path "%s" is not in the list of allowed paths (IMPORT_DIR_ALLOWLIST).', $directory));
  67. app('log')->error(sprintf('Exit code is %s.', ExitCode::INVALID_PATH->name));
  68. return ExitCode::INVALID_PATH->value;
  69. }
  70. }
  71. // validate file path
  72. if ('' !== $file) {
  73. $directory = dirname($file);
  74. if (!$this->isAllowedPath($directory)) {
  75. $this->error(sprintf('Path "%s" is not in the list of allowed paths (IMPORT_DIR_ALLOWLIST).', $directory));
  76. app('log')->error(sprintf('Exit code is %s.', ExitCode::NOT_ALLOWED_PATH->name));
  77. return ExitCode::NOT_ALLOWED_PATH->value;
  78. }
  79. }
  80. if (!file_exists($config) || !is_file($config)) {
  81. $message = sprintf('The importer can\'t import: configuration file "%s" does not exist or could not be read.', $config);
  82. $this->error($message);
  83. app('log')->error($message);
  84. app('log')->error(sprintf('Exit code is %s.', ExitCode::CANNOT_READ_CONFIG->name));
  85. return ExitCode::CANNOT_READ_CONFIG->value;
  86. }
  87. $jsonResult = $this->verifyJSON($config);
  88. if (false === $jsonResult) {
  89. $message = 'The importer can\'t import: could not decode the JSON in the config file.';
  90. $this->error($message);
  91. app('log')->error(sprintf('Exit code is %s.', ExitCode::CANNOT_PARSE_CONFIG->name));
  92. return ExitCode::CANNOT_PARSE_CONFIG->value;
  93. }
  94. $configuration = Configuration::fromArray(json_decode((string) file_get_contents($config), true));
  95. if ('file' === $configuration->getFlow() && (!file_exists($file) || !is_file($file))) {
  96. $message = sprintf('The importer can\'t import: importable file "%s" does not exist or could not be read.', $file);
  97. $this->error($message);
  98. app('log')->error($message);
  99. app('log')->error(sprintf('Exit code is %s.', ExitCode::IMPORTABLE_FILE_NOT_FOUND->name));
  100. return ExitCode::IMPORTABLE_FILE_NOT_FOUND->value;
  101. }
  102. $configuration->updateDateRange();
  103. $this->line('The import routine is about to start.');
  104. $this->line('This is invisible and may take quite some time.');
  105. $this->line('Once finished, you will see a list of errors, warnings and messages (if applicable).');
  106. $this->line('--------');
  107. $this->line('Running...');
  108. // first do conversion based on the file:
  109. $this->startConversion($configuration, $file);
  110. $this->reportConversion();
  111. // crash here if the conversion failed.
  112. $exitCode = ExitCode::SUCCESS->value;
  113. if (0 !== count($this->conversionErrors)) {
  114. app('log')->error(sprintf('Exit code is %s.', ExitCode::TOO_MANY_ERRORS_PROCESSING->name));
  115. $exitCode = ExitCode::TOO_MANY_ERRORS_PROCESSING->value;
  116. // could still be that there were simply no transactions (from GoCardless). This can result
  117. // in another exit code.
  118. if ($this->isNothingDownloaded()) {
  119. app('log')->error(sprintf('Exit code changed to %s.', ExitCode::NOTHING_WAS_IMPORTED->name));
  120. $exitCode = ExitCode::NOTHING_WAS_IMPORTED->value;
  121. }
  122. $this->error('There are many errors in the data conversion. The import will stop here.');
  123. }
  124. if (0 === count($this->conversionErrors)) {
  125. $this->line(sprintf('Done converting from file %s using configuration %s.', $file, $config));
  126. $this->startImport($configuration);
  127. $this->reportImport();
  128. $this->line('Done!');
  129. }
  130. $this->reportBalanceDifferences($configuration);
  131. // merge things:
  132. $messages = array_merge($this->importMessages, $this->conversionMessages);
  133. $warnings = array_merge($this->importWarnings, $this->conversionWarnings);
  134. $errors = array_merge($this->importErrors, $this->conversionErrors);
  135. event(new ImportedTransactions($messages, $warnings, $errors, $this->conversionRateLimits));
  136. if (0 !== count($this->importErrors)) {
  137. $exitCode = ExitCode::GENERAL_ERROR->value;
  138. app('log')->error(sprintf('Exit code is %s.', ExitCode::GENERAL_ERROR->name));
  139. }
  140. if (0 === count($messages) && 0 === count($warnings) && 0 === count($errors)) {
  141. $exitCode = ExitCode::NOTHING_WAS_IMPORTED->value;
  142. app('log')->error(sprintf('Exit code is %s.', ExitCode::NOTHING_WAS_IMPORTED->name));
  143. }
  144. if ($exitCode === ExitCode::SUCCESS->value) {
  145. app('log')->debug(sprintf('Exit code is %s.', ExitCode::SUCCESS->name));
  146. }
  147. return $exitCode;
  148. }
  149. }