. */ declare(strict_types=1); namespace FireflyIII\Console\Commands; use Exception; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Import\Prerequisites\PrerequisitesInterface; use FireflyIII\Import\Routine\RoutineInterface; use FireflyIII\Import\Storage\ImportArrayStorage; use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface; use FireflyIII\Repositories\User\UserRepositoryInterface; use Illuminate\Console\Command; use Log; use Preferences; /** * Class CreateImport. */ class CreateImport extends Command { use VerifiesAccessToken; /** * The console command description. * * @var string */ protected $description = 'Use this command to create a new import. Your user ID can be found on the /profile page.'; /** * The name and signature of the console command. * * @var string */ protected $signature = 'firefly:create-import {file? : The file to import.} {configuration? : The configuration file to use for the import.} {--type=csv : The file type of the import.} {--provider=file : The file type of the import.} {--user=1 : The user ID that the import should import for.} {--token= : The user\'s access token.} {--start : Starts the job immediately.}'; /** * Run the command. * * @throws FireflyException */ public function handle(): int { if (!$this->verifyAccessToken()) { $this->errorLine('Invalid access token.'); return 1; } /** @var UserRepositoryInterface $userRepository */ $userRepository = app(UserRepositoryInterface::class); $file = (string)$this->argument('file'); $configuration = (string)$this->argument('configuration'); $user = $userRepository->findNull((int)$this->option('user')); $cwd = getcwd(); $type = strtolower((string)$this->option('type')); $provider = strtolower((string)$this->option('provider')); $configurationData = []; if (!$this->validArguments()) { $this->errorLine('Invalid arguments.'); return 1; } if (\strlen($configuration) > 0) { $configurationData = json_decode(file_get_contents($configuration), true); if (null === $configurationData) { $this->errorLine(sprintf('Firefly III cannot read the contents of configuration file "%s" (working directory: "%s").', $configuration, $cwd)); return 1; } } $this->infoLine(sprintf('Going to create a job to import file: %s', $file)); $this->infoLine(sprintf('Using configuration file: %s', $configuration)); $this->infoLine(sprintf('Import into user: #%d (%s)', $user->id, $user->email)); $this->infoLine(sprintf('Type of import: %s', $provider)); /** @var ImportJobRepositoryInterface $jobRepository */ $jobRepository = app(ImportJobRepositoryInterface::class); $jobRepository->setUser($user); $importJob = $jobRepository->create($provider); $this->infoLine(sprintf('Created job "%s"', $importJob->key)); // make sure that job has no prerequisites. if ((bool)config(sprintf('import.has_prereq.%s', $provider))) { // make prerequisites thing. $class = (string)config(sprintf('import.prerequisites.%s', $provider)); if (!class_exists($class)) { throw new FireflyException(sprintf('No class to handle prerequisites for "%s".', $provider)); // @codeCoverageIgnore } /** @var PrerequisitesInterface $object */ $object = app($class); $object->setUser($user); if (!$object->isComplete()) { $this->errorLine(sprintf('Import provider "%s" has prerequisites that can only be filled in using the browser.', $provider)); return 1; } } // store file as attachment. if (\strlen($file) > 0) { $messages = $jobRepository->storeCLIUpload($importJob, 'import_file', $file); if ($messages->count() > 0) { $this->errorLine($messages->first()); return 1; } $this->infoLine('File content saved.'); } $this->infoLine('Job configuration saved.'); $jobRepository->setConfiguration($importJob, $configurationData); $jobRepository->setStatus($importJob, 'ready_to_run'); if (true === $this->option('start')) { $this->infoLine('The has started. The process is not visible. Please wait.'); Log::debug('Go for import!'); // run it! $key = sprintf('import.routine.%s', $provider); $className = config($key); if (null === $className || !class_exists($className)) { // @codeCoverageIgnoreStart $this->errorLine(sprintf('No routine for provider "%s"', $provider)); return 1; // @codeCoverageIgnoreEnd } // keep repeating this call until job lands on "provider_finished" $valid = ['provider_finished']; $count = 0; while (!\in_array($importJob->status, $valid, true) && $count < 6) { Log::debug(sprintf('Now in loop #%d.', $count + 1)); /** @var RoutineInterface $routine */ $routine = app($className); $routine->setImportJob($importJob); try { $routine->run(); } catch (FireflyException|Exception $e) { $message = 'The import routine crashed: ' . $e->getMessage(); Log::error($message); Log::error($e->getTraceAsString()); // set job errored out: $jobRepository->setStatus($importJob, 'error'); $this->errorLine($message); return 1; } $count++; } if ($importJob->status === 'provider_finished') { $this->infoLine('Import has finished. Please wait for storage of data.'); // set job to be storing data: $jobRepository->setStatus($importJob, 'storing_data'); /** @var ImportArrayStorage $storage */ $storage = app(ImportArrayStorage::class); $storage->setImportJob($importJob); try { $storage->store(); } catch (FireflyException|Exception $e) { $message = 'The import routine crashed: ' . $e->getMessage(); Log::error($message); Log::error($e->getTraceAsString()); // set job errored out: $jobRepository->setStatus($importJob, 'error'); $this->errorLine($message); return 1; } // set storage to be finished: $jobRepository->setStatus($importJob, 'storage_finished'); } // give feedback: $this->infoLine('Job has finished.'); if (null !== $importJob->tag) { $this->infoLine(sprintf('%d transaction(s) have been imported.', $importJob->tag->transactionJournals->count())); $this->infoLine(sprintf('You can find your transactions under tag "%s"', $importJob->tag->tag)); } if (null === $importJob->tag) { $this->errorLine('No transactions have been imported :(.'); } if (\count($importJob->errors) > 0) { $this->infoLine(sprintf('%d error(s) occurred:', \count($importJob->errors))); foreach ($importJob->errors as $err) { $this->errorLine('- ' . $err); } } } // clear cache for user: Preferences::setForUser($user, 'lastActivity', microtime()); return 0; } /** * @param string $message * @param array|null $data */ private function errorLine(string $message, array $data = null): void { Log::error($message, $data ?? []); $this->error($message); } /** * @param string $message * @param array $data */ private function infoLine(string $message, array $data = null): void { Log::info($message, $data ?? []); $this->line($message); } /** * Verify user inserts correct arguments. * * @noinspection MultipleReturnStatementsInspection * @return bool */ private function validArguments(): bool { $file = $this->argument('file'); $configuration = $this->argument('configuration'); $cwd = getcwd(); $validTypes = config('import.options.file.import_formats'); $type = strtolower($this->option('type')); $provider = strtolower($this->option('provider')); $enabled = (bool)config(sprintf('import.enabled.%s', $provider)); if (false === $enabled) { $this->errorLine(sprintf('Provider "%s" is not enabled.', $provider)); return false; } if ($provider === 'file' && !\in_array($type, $validTypes, true)) { $this->errorLine(sprintf('Cannot import file of type "%s"', $type)); return false; } if ($provider === 'file' && !file_exists($file)) { $this->errorLine(sprintf('Firefly III cannot find file "%s" (working directory: "%s").', $file, $cwd)); return false; } if ($provider === 'file' && !file_exists($configuration)) { $this->errorLine(sprintf('Firefly III cannot find configuration file "%s" (working directory: "%s").', $configuration, $cwd)); return false; } return true; } }