2019-12-27 03:59:31 -06:00
|
|
|
<?php
|
2020-06-30 12:05:35 -05:00
|
|
|
|
2019-12-27 03:59:31 -06:00
|
|
|
/**
|
|
|
|
* ExportData.php
|
2020-01-23 13:35:02 -06:00
|
|
|
* Copyright (c) 2020 james@firefly-iii.org
|
2019-12-27 03:59:31 -06:00
|
|
|
*
|
|
|
|
* This file is part of Firefly III (https://github.com/firefly-iii).
|
|
|
|
*
|
|
|
|
* This program is free software: you can redistribute it and/or modify
|
|
|
|
* it under the terms of the GNU Affero General Public License as
|
|
|
|
* published by the Free Software Foundation, either version 3 of the
|
|
|
|
* License, or (at your option) any later version.
|
|
|
|
*
|
|
|
|
* This program is distributed in the hope that it will be useful,
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
* GNU Affero General Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
|
2020-06-30 12:05:35 -05:00
|
|
|
declare(strict_types=1);
|
|
|
|
|
2019-12-27 14:31:25 -06:00
|
|
|
namespace FireflyIII\Console\Commands\Export;
|
2019-12-27 03:59:31 -06:00
|
|
|
|
2019-12-27 14:31:25 -06:00
|
|
|
use Carbon\Carbon;
|
2020-03-17 08:54:25 -05:00
|
|
|
use Exception;
|
2019-12-27 14:31:25 -06:00
|
|
|
use FireflyIII\Console\Commands\VerifiesAccessToken;
|
|
|
|
use FireflyIII\Exceptions\FireflyException;
|
2021-04-26 23:42:07 -05:00
|
|
|
use FireflyIII\Models\Account;
|
2019-12-27 14:31:25 -06:00
|
|
|
use FireflyIII\Models\AccountType;
|
|
|
|
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
|
|
|
|
use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
|
|
|
|
use FireflyIII\Support\Export\ExportDataGenerator;
|
2019-12-27 03:59:31 -06:00
|
|
|
use Illuminate\Console\Command;
|
2019-12-27 14:31:25 -06:00
|
|
|
use Illuminate\Support\Collection;
|
|
|
|
use InvalidArgumentException;
|
2020-03-17 08:54:25 -05:00
|
|
|
use League\Csv\CannotInsertRecord;
|
2019-12-27 14:31:25 -06:00
|
|
|
use Log;
|
2019-12-27 03:59:31 -06:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Class ExportData
|
|
|
|
*/
|
|
|
|
class ExportData extends Command
|
|
|
|
{
|
2019-12-27 14:31:25 -06:00
|
|
|
use VerifiesAccessToken;
|
|
|
|
|
2019-12-27 03:59:31 -06:00
|
|
|
/**
|
|
|
|
* The console command description.
|
|
|
|
*
|
|
|
|
* @var string
|
|
|
|
*/
|
2019-12-27 14:31:25 -06:00
|
|
|
protected $description = 'Command to export data from Firefly III.';
|
2019-12-27 03:59:31 -06:00
|
|
|
/**
|
|
|
|
* The name and signature of the console command.
|
|
|
|
*
|
|
|
|
* @var string
|
|
|
|
*/
|
2020-11-01 23:20:49 -06:00
|
|
|
protected $signature = 'firefly-iii:export-data
|
2019-12-27 14:31:25 -06:00
|
|
|
{--user=1 : The user ID that the export should run for.}
|
|
|
|
{--token= : The user\'s access token.}
|
|
|
|
{--start= : First transaction to export. Defaults to your very first transaction. Only applies to transaction export.}
|
|
|
|
{--end= : Last transaction to export. Defaults to today. Only applies to transaction export.}
|
|
|
|
{--accounts= : From which accounts or liabilities to export. Only applies to transaction export. Defaults to all of your asset accounts.}
|
|
|
|
{--export_directory=./ : Where to store the export files.}
|
|
|
|
{--export-transactions : Create a file with all your transactions and their meta data. This flag and the other flags can be combined.}
|
|
|
|
{--export-accounts : Create a file with all your accounts and some meta data.}
|
|
|
|
{--export-budgets : Create a file with all your budgets and some meta data.}
|
|
|
|
{--export-categories : Create a file with all your categories and some meta data.}
|
|
|
|
{--export-tags : Create a file with all your tags and some meta data.}
|
|
|
|
{--export-recurring : Create a file with all your recurring transactions and some meta data.}
|
|
|
|
{--export-rules : Create a file with all your rules and some meta data.}
|
|
|
|
{--export-bills : Create a file with all your bills and some meta data.}
|
|
|
|
{--export-piggies : Create a file with all your piggy banks and some meta data.}
|
|
|
|
{--force : Force overwriting of previous exports if found.}';
|
2020-11-01 23:20:49 -06:00
|
|
|
private AccountRepositoryInterface $accountRepository;
|
|
|
|
private JournalRepositoryInterface $journalRepository;
|
2019-12-27 03:59:31 -06:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Execute the console command.
|
|
|
|
*
|
2020-03-17 08:54:25 -05:00
|
|
|
* @return int
|
2020-11-01 23:20:49 -06:00
|
|
|
* @throws FireflyException
|
2019-12-27 03:59:31 -06:00
|
|
|
*/
|
2019-12-27 14:31:25 -06:00
|
|
|
public function handle(): int
|
2019-12-27 03:59:31 -06:00
|
|
|
{
|
2019-12-27 14:31:25 -06:00
|
|
|
// verify access token
|
|
|
|
if (!$this->verifyAccessToken()) {
|
2020-01-10 10:12:38 -06:00
|
|
|
$this->error('Invalid access token. Check /profile.');
|
2019-12-27 14:31:25 -06:00
|
|
|
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
// set up repositories.
|
|
|
|
$this->stupidLaravel();
|
2021-05-28 16:13:38 -05:00
|
|
|
$user = $this->getUser();
|
|
|
|
$this->journalRepository->setUser($user);
|
|
|
|
$this->accountRepository->setUser($user);
|
2019-12-27 14:31:25 -06:00
|
|
|
// get the options.
|
|
|
|
try {
|
|
|
|
$options = $this->parseOptions();
|
|
|
|
} catch (FireflyException $e) {
|
|
|
|
$this->error(sprintf('Could not work with your options: %s', $e));
|
|
|
|
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
// make export object and configure it.
|
|
|
|
/** @var ExportDataGenerator $exporter */
|
|
|
|
$exporter = app(ExportDataGenerator::class);
|
2021-05-28 16:13:38 -05:00
|
|
|
$exporter->setUser($user);
|
2021-03-03 23:28:16 -06:00
|
|
|
|
2019-12-27 14:31:25 -06:00
|
|
|
$exporter->setStart($options['start']);
|
|
|
|
$exporter->setEnd($options['end']);
|
2021-03-03 23:28:16 -06:00
|
|
|
$exporter->setAccounts($options['accounts']);
|
2019-12-27 14:31:25 -06:00
|
|
|
$exporter->setExportTransactions($options['export']['transactions']);
|
|
|
|
$exporter->setExportAccounts($options['export']['accounts']);
|
|
|
|
$exporter->setExportBudgets($options['export']['budgets']);
|
|
|
|
$exporter->setExportCategories($options['export']['categories']);
|
|
|
|
$exporter->setExportTags($options['export']['tags']);
|
|
|
|
$exporter->setExportRecurring($options['export']['recurring']);
|
|
|
|
$exporter->setExportRules($options['export']['rules']);
|
|
|
|
$exporter->setExportBills($options['export']['bills']);
|
|
|
|
$exporter->setExportPiggies($options['export']['piggies']);
|
|
|
|
$data = $exporter->export();
|
2021-06-12 12:32:34 -05:00
|
|
|
if (empty($data)) {
|
2019-12-27 14:38:09 -06:00
|
|
|
$this->error('You must export *something*. Use --export-transactions or another option. See docs.firefly-iii.org');
|
|
|
|
}
|
2020-11-01 23:20:49 -06:00
|
|
|
$returnCode = 0;
|
2021-06-12 12:32:34 -05:00
|
|
|
if (!empty($data)) {
|
2020-11-01 23:20:49 -06:00
|
|
|
try {
|
|
|
|
$this->exportData($options, $data);
|
|
|
|
} catch (FireflyException $e) {
|
|
|
|
$this->error(sprintf('Could not store data: %s', $e->getMessage()));
|
2019-12-27 14:38:09 -06:00
|
|
|
|
2020-11-01 23:20:49 -06:00
|
|
|
$returnCode = 1;
|
|
|
|
}
|
2019-12-27 14:31:25 -06:00
|
|
|
}
|
2020-03-21 09:43:41 -05:00
|
|
|
|
2020-11-01 23:20:49 -06:00
|
|
|
return $returnCode;
|
2019-12-27 03:59:31 -06:00
|
|
|
}
|
2019-12-27 14:31:25 -06:00
|
|
|
|
|
|
|
/**
|
2021-03-11 23:30:40 -06:00
|
|
|
* Laravel will execute ALL __construct() methods for ALL commands whenever a SINGLE command is
|
|
|
|
* executed. This leads to noticeable slow-downs and class calls. To prevent this, this method should
|
|
|
|
* be called from the handle method instead of using the constructor to initialize the command.
|
2019-12-27 14:31:25 -06:00
|
|
|
*
|
2021-03-11 23:30:40 -06:00
|
|
|
* @codeCoverageIgnore
|
2019-12-27 14:31:25 -06:00
|
|
|
*/
|
2021-03-11 23:30:40 -06:00
|
|
|
private function stupidLaravel(): void
|
2019-12-27 14:31:25 -06:00
|
|
|
{
|
2021-03-11 23:30:40 -06:00
|
|
|
$this->journalRepository = app(JournalRepositoryInterface::class);
|
|
|
|
$this->accountRepository = app(AccountRepositoryInterface::class);
|
2019-12-27 14:31:25 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2021-03-11 23:30:40 -06:00
|
|
|
* @return array
|
2020-11-01 23:20:49 -06:00
|
|
|
* @throws FireflyException
|
2019-12-27 14:31:25 -06:00
|
|
|
*/
|
2021-03-11 23:30:40 -06:00
|
|
|
private function parseOptions(): array
|
2019-12-27 14:31:25 -06:00
|
|
|
{
|
2021-04-12 08:28:06 -05:00
|
|
|
$start = $this->getDateParameter('start');
|
|
|
|
$end = $this->getDateParameter('end');
|
2021-03-11 23:30:40 -06:00
|
|
|
$accounts = $this->getAccountsParameter();
|
|
|
|
$export = $this->getExportDirectory();
|
2019-12-27 14:31:25 -06:00
|
|
|
|
2021-03-11 23:30:40 -06:00
|
|
|
return [
|
|
|
|
'export' => [
|
|
|
|
'transactions' => $this->option('export-transactions'),
|
|
|
|
'accounts' => $this->option('export-accounts'),
|
|
|
|
'budgets' => $this->option('export-budgets'),
|
|
|
|
'categories' => $this->option('export-categories'),
|
|
|
|
'tags' => $this->option('export-tags'),
|
|
|
|
'recurring' => $this->option('export-recurring'),
|
|
|
|
'rules' => $this->option('export-rules'),
|
|
|
|
'bills' => $this->option('export-bills'),
|
|
|
|
'piggies' => $this->option('export-piggies'),
|
|
|
|
],
|
|
|
|
'start' => $start,
|
|
|
|
'end' => $end,
|
|
|
|
'accounts' => $accounts,
|
|
|
|
'directory' => $export,
|
|
|
|
'force' => $this->option('force'),
|
|
|
|
];
|
2019-12-27 14:31:25 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param string $field
|
|
|
|
*
|
2020-03-17 08:54:25 -05:00
|
|
|
* @return Carbon
|
2020-11-01 23:20:49 -06:00
|
|
|
* @throws Exception
|
2019-12-27 14:31:25 -06:00
|
|
|
*/
|
|
|
|
private function getDateParameter(string $field): Carbon
|
|
|
|
{
|
2021-03-11 23:30:40 -06:00
|
|
|
$date = Carbon::now()->subYear();
|
2020-11-01 23:20:49 -06:00
|
|
|
$error = false;
|
2019-12-27 14:31:25 -06:00
|
|
|
if (null !== $this->option($field)) {
|
|
|
|
try {
|
2021-04-10 23:49:46 -05:00
|
|
|
$date = Carbon::createFromFormat('!Y-m-d', $this->option($field));
|
2019-12-27 14:31:25 -06:00
|
|
|
} catch (InvalidArgumentException $e) {
|
|
|
|
Log::error($e->getMessage());
|
2020-01-10 10:12:38 -06:00
|
|
|
$this->error(sprintf('%s date "%s" must be formatted YYYY-MM-DD. Field will be ignored.', $field, $this->option('start')));
|
2020-11-01 23:20:49 -06:00
|
|
|
$error = true;
|
2019-12-27 14:31:25 -06:00
|
|
|
}
|
|
|
|
}
|
2021-04-10 23:49:46 -05:00
|
|
|
|
|
|
|
if (true === $error && 'start' === $field) {
|
2019-12-27 14:31:25 -06:00
|
|
|
$journal = $this->journalRepository->firstNull();
|
2020-01-10 10:12:38 -06:00
|
|
|
$date = null === $journal ? Carbon::now()->subYear() : $journal->date;
|
2019-12-27 14:31:25 -06:00
|
|
|
$date->startOfDay();
|
|
|
|
}
|
2021-04-10 23:49:46 -05:00
|
|
|
if (true === $error && 'end' === $field) {
|
2020-09-11 00:12:33 -05:00
|
|
|
$date = today(config('app.timezone'));
|
2019-12-27 14:31:25 -06:00
|
|
|
$date->endOfDay();
|
|
|
|
}
|
2021-04-10 23:49:46 -05:00
|
|
|
if ('end' === $field) {
|
|
|
|
$date->endOfDay();
|
|
|
|
}
|
2019-12-27 14:31:25 -06:00
|
|
|
|
|
|
|
return $date;
|
|
|
|
}
|
|
|
|
|
2021-03-11 23:30:40 -06:00
|
|
|
/**
|
|
|
|
* @return Collection
|
|
|
|
* @throws FireflyException
|
|
|
|
*/
|
|
|
|
private function getAccountsParameter(): Collection
|
|
|
|
{
|
|
|
|
$final = new Collection;
|
|
|
|
$accounts = new Collection;
|
|
|
|
$accountList = $this->option('accounts');
|
|
|
|
$types = [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE];
|
|
|
|
if (null !== $accountList && '' !== (string)$accountList) {
|
|
|
|
$accountIds = explode(',', $accountList);
|
|
|
|
$accounts = $this->accountRepository->getAccountsById($accountIds);
|
|
|
|
}
|
|
|
|
if (null === $accountList) {
|
|
|
|
$accounts = $this->accountRepository->getAccountsByType($types);
|
|
|
|
}
|
|
|
|
// filter accounts,
|
2021-04-26 23:42:07 -05:00
|
|
|
/** @var Account $account */
|
2021-03-11 23:30:40 -06:00
|
|
|
foreach ($accounts as $account) {
|
|
|
|
if (in_array($account->accountType->type, $types, true)) {
|
|
|
|
$final->push($account);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (0 === $final->count()) {
|
|
|
|
throw new FireflyException('Ended up with zero valid accounts to export from.');
|
|
|
|
}
|
|
|
|
|
|
|
|
return $final;
|
|
|
|
}
|
|
|
|
|
2019-12-27 14:31:25 -06:00
|
|
|
/**
|
2020-11-01 23:20:49 -06:00
|
|
|
* @return string
|
2019-12-27 14:31:25 -06:00
|
|
|
* @throws FireflyException
|
2020-03-25 13:25:50 -05:00
|
|
|
*
|
2019-12-27 14:31:25 -06:00
|
|
|
*/
|
|
|
|
private function getExportDirectory(): string
|
|
|
|
{
|
2020-11-01 23:20:49 -06:00
|
|
|
$directory = (string)$this->option('export_directory');
|
2019-12-27 14:31:25 -06:00
|
|
|
if (null === $directory) {
|
|
|
|
$directory = './';
|
|
|
|
}
|
|
|
|
if (!is_writable($directory)) {
|
|
|
|
throw new FireflyException(sprintf('Directory "%s" isn\'t writeable.', $directory));
|
|
|
|
}
|
|
|
|
|
|
|
|
return $directory;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2021-03-11 23:30:40 -06:00
|
|
|
* @param array $options
|
|
|
|
* @param array $data
|
2019-12-27 14:31:25 -06:00
|
|
|
*
|
2021-03-11 23:30:40 -06:00
|
|
|
* @throws FireflyException
|
2019-12-27 14:31:25 -06:00
|
|
|
*/
|
2021-03-11 23:30:40 -06:00
|
|
|
private function exportData(array $options, array $data): void
|
2019-12-27 14:31:25 -06:00
|
|
|
{
|
2021-03-11 23:30:40 -06:00
|
|
|
$date = date('Y_m_d');
|
|
|
|
foreach ($data as $key => $content) {
|
|
|
|
$file = sprintf('%s%s_%s.csv', $options['directory'], $date, $key);
|
|
|
|
if (false === $options['force'] && file_exists($file)) {
|
|
|
|
throw new FireflyException(sprintf('File "%s" exists already. Use --force to overwrite.', $file));
|
|
|
|
}
|
|
|
|
if (true === $options['force'] && file_exists($file)) {
|
|
|
|
$this->warn(sprintf('File "%s" exists already but will be replaced.', $file));
|
|
|
|
}
|
|
|
|
// continue to write to file.
|
|
|
|
file_put_contents($file, $content);
|
|
|
|
$this->info(sprintf('Wrote %s-export to file "%s".', $key, $file));
|
|
|
|
}
|
2019-12-27 14:31:25 -06:00
|
|
|
}
|
2019-12-27 03:59:31 -06:00
|
|
|
}
|