mirror of
https://github.com/chenasraf/nextcloud-autocurrency.git
synced 2026-05-17 17:28:06 +00:00
feat: add setting for history retention period
This commit is contained in:
@@ -65,6 +65,7 @@ class ApiController extends OCSController {
|
||||
* @return DataResponse<Http::STATUS_OK, array{
|
||||
* last_update: non-empty-string|null,
|
||||
* interval: int,
|
||||
* retention_days: int,
|
||||
* }, array{}>
|
||||
*
|
||||
* 200: Data returned
|
||||
@@ -77,9 +78,10 @@ class ApiController extends OCSController {
|
||||
}
|
||||
|
||||
$interval = $this->config->getValueInt(AppInfo\Application::APP_ID, 'cron_interval', 24);
|
||||
$retentionDays = $this->config->getValueInt(AppInfo\Application::APP_ID, 'retention_days', 30);
|
||||
|
||||
return new DataResponse(
|
||||
['last_update' => $lastUpdate, 'interval' => $interval]
|
||||
['last_update' => $lastUpdate, 'interval' => $interval, 'retention_days' => $retentionDays]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -140,7 +142,7 @@ class ApiController extends OCSController {
|
||||
/**
|
||||
* Update auto currency settings
|
||||
*
|
||||
* @param array{interval: int} $data Data to update
|
||||
* @param array{interval: int, retention_days?: int} $data Data to update
|
||||
* @return DataResponse<Http::STATUS_OK, array{status:non-empty-string}, array{}>
|
||||
*
|
||||
* 200: Data returned
|
||||
@@ -149,6 +151,16 @@ class ApiController extends OCSController {
|
||||
public function updateSettings(mixed $data): DataResponse {
|
||||
$interval = $data['interval'];
|
||||
$this->config->setValueInt(AppInfo\Application::APP_ID, 'cron_interval', $interval);
|
||||
|
||||
if (isset($data['retention_days'])) {
|
||||
$retentionDays = (int)$data['retention_days'];
|
||||
// Ensure it's not negative (0 = no limit, >0 = days to keep)
|
||||
if ($retentionDays < 0) {
|
||||
$retentionDays = 0;
|
||||
}
|
||||
$this->config->setValueInt(AppInfo\Application::APP_ID, 'retention_days', $retentionDays);
|
||||
}
|
||||
|
||||
return new DataResponse(
|
||||
['status' => 'OK']
|
||||
);
|
||||
|
||||
@@ -20,7 +20,6 @@ class FetchCurrenciesJob extends TimedJob {
|
||||
$this->logger = $logger;
|
||||
$this->config = $config;
|
||||
|
||||
// Run once a day
|
||||
$interval = $this->config->getValueInt(AppInfo\Application::APP_ID, 'cron_interval', 24);
|
||||
$this->setInterval(3600 * $interval);
|
||||
$this->setTimeSensitivity(\OCP\BackgroundJob\IJob::TIME_INSENSITIVE);
|
||||
|
||||
35
lib/Cron/RemoveOldHistoryTask.php
Normal file
35
lib/Cron/RemoveOldHistoryTask.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
// SPDX-FileCopyrightText: Chen Asraf <contact@casraf.dev>
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
namespace OCA\AutoCurrency\Cron;
|
||||
|
||||
use OCA\AutoCurrency\Service\RemoveOldHistoryService;
|
||||
use OCP\AppFramework\Utility\ITimeFactory;
|
||||
use OCP\BackgroundJob\TimedJob;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class RemoveOldHistoryTask extends TimedJob {
|
||||
public function __construct(
|
||||
ITimeFactory $time,
|
||||
private LoggerInterface $logger,
|
||||
private RemoveOldHistoryService $removeOldHistoryService,
|
||||
) {
|
||||
parent::__construct($time);
|
||||
$this->setInterval(3600 * 24);
|
||||
}
|
||||
|
||||
protected function run($arguments): void {
|
||||
$this->logger->debug('RemoveOldHistoryTask: Starting cleanup of old history records');
|
||||
|
||||
try {
|
||||
$deletedCount = $this->removeOldHistoryService->removeOldHistory();
|
||||
$this->logger->info("RemoveOldHistoryTask: Successfully removed {$deletedCount} old history records");
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->error('RemoveOldHistoryTask: Failed to remove old history records: ' . $e->getMessage(), ['exception' => $e]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -94,4 +94,20 @@ class AutocurrencyRateHistoryMapper extends QBMapper {
|
||||
|
||||
return $this->findEntities($qb);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all history records older than the specified date
|
||||
*
|
||||
* @param DateTimeInterface $cutoffDate Delete records with fetched_at before this date
|
||||
* @return int Number of rows deleted
|
||||
*/
|
||||
public function deleteOlderThan(DateTimeInterface $cutoffDate): int {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->delete($this->getTableName())
|
||||
->where(
|
||||
$qb->expr()->lt('fetched_at', $qb->createNamedParameter($cutoffDate->format('Y-m-d H:i:s')))
|
||||
);
|
||||
|
||||
return $qb->executeStatement();
|
||||
}
|
||||
}
|
||||
|
||||
51
lib/Service/RemoveOldHistoryService.php
Normal file
51
lib/Service/RemoveOldHistoryService.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
// SPDX-FileCopyrightText: Chen Asraf <contact@casraf.dev>
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
namespace OCA\AutoCurrency\Service;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use OCA\AutoCurrency\AppInfo;
|
||||
use OCA\AutoCurrency\Db\AutocurrencyRateHistoryMapper;
|
||||
use OCP\IAppConfig;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class RemoveOldHistoryService {
|
||||
public function __construct(
|
||||
private LoggerInterface $logger,
|
||||
private IAppConfig $config,
|
||||
private AutocurrencyRateHistoryMapper $historyMapper,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove old history records based on the retention_days setting
|
||||
* If retention_days is 0, no records are deleted (no limit)
|
||||
* If retention_days is > 0, records older than that many days are deleted
|
||||
*
|
||||
* @return int Number of records deleted
|
||||
*/
|
||||
public function removeOldHistory(): int {
|
||||
$retentionDays = $this->config->getValueInt(AppInfo\Application::APP_ID, 'retention_days', 30);
|
||||
|
||||
// If retention is 0, don't delete anything (no limit)
|
||||
if ($retentionDays === 0) {
|
||||
$this->logger->debug('History retention is set to 0 (no limit), skipping cleanup');
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Calculate the cutoff date
|
||||
$cutoffDate = new DateTimeImmutable("-{$retentionDays} days");
|
||||
$this->logger->info("Removing history records older than {$retentionDays} days (before {$cutoffDate->format('Y-m-d H:i:s')})");
|
||||
|
||||
// Delete old records
|
||||
$deletedCount = $this->historyMapper->deleteOlderThan($cutoffDate);
|
||||
$this->logger->info("Removed {$deletedCount} old history records");
|
||||
|
||||
return $deletedCount;
|
||||
}
|
||||
}
|
||||
@@ -100,7 +100,8 @@
|
||||
"type": "object",
|
||||
"required": [
|
||||
"last_update",
|
||||
"interval"
|
||||
"interval",
|
||||
"retention_days"
|
||||
],
|
||||
"properties": {
|
||||
"last_update": {
|
||||
@@ -111,6 +112,10 @@
|
||||
"interval": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"retention_days": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -214,6 +219,10 @@
|
||||
"interval": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"retention_days": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +100,8 @@
|
||||
"type": "object",
|
||||
"required": [
|
||||
"last_update",
|
||||
"interval"
|
||||
"interval",
|
||||
"retention_days"
|
||||
],
|
||||
"properties": {
|
||||
"last_update": {
|
||||
@@ -111,6 +112,10 @@
|
||||
"interval": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"retention_days": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -214,6 +219,10 @@
|
||||
"interval": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"retention_days": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,110 +11,125 @@
|
||||
<p v-html="strings.customCurrenciesHelp" />
|
||||
</NcNoteCard>
|
||||
|
||||
<div class="custom-currencies-list">
|
||||
<div
|
||||
v-for="(currency, index) in customCurrencies"
|
||||
:key="currency.tempId || currency.id"
|
||||
class="currency-item"
|
||||
>
|
||||
<div class="currency-fields">
|
||||
<div class="field-row">
|
||||
<NcTextField
|
||||
v-model="currency.code"
|
||||
:label="strings.currencyCode"
|
||||
:placeholder="strings.currencyCodePlaceholder"
|
||||
required
|
||||
:disabled="loading"
|
||||
/>
|
||||
<NcTextField
|
||||
v-model="currency.symbol"
|
||||
:label="strings.currencySymbol"
|
||||
:placeholder="strings.currencySymbolPlaceholder"
|
||||
:disabled="loading"
|
||||
/>
|
||||
<div class="settings-section">
|
||||
<div class="custom-currencies-list">
|
||||
<div
|
||||
v-for="(currency, index) in customCurrencies"
|
||||
:key="currency.tempId || currency.id"
|
||||
class="currency-item"
|
||||
>
|
||||
<div class="currency-fields">
|
||||
<div class="field-row">
|
||||
<NcTextField
|
||||
v-model="currency.code"
|
||||
:label="strings.currencyCode"
|
||||
:placeholder="strings.currencyCodePlaceholder"
|
||||
required
|
||||
:disabled="loading"
|
||||
/>
|
||||
<NcTextField
|
||||
v-model="currency.symbol"
|
||||
:label="strings.currencySymbol"
|
||||
:placeholder="strings.currencySymbolPlaceholder"
|
||||
:disabled="loading"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="field-row">
|
||||
<NcTextField
|
||||
v-model="currency.api_endpoint"
|
||||
:label="strings.apiEndpoint"
|
||||
type="url"
|
||||
:placeholder="strings.apiEndpointPlaceholder"
|
||||
required
|
||||
:disabled="loading"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="field-row">
|
||||
<NcTextField
|
||||
v-model="currency.api_key"
|
||||
:label="strings.apiKey"
|
||||
type="password"
|
||||
:placeholder="strings.apiKeyPlaceholder"
|
||||
:disabled="loading"
|
||||
/>
|
||||
<NcTextField
|
||||
v-model="currency.json_path"
|
||||
:label="strings.jsonPath"
|
||||
:placeholder="strings.jsonPathPlaceholder"
|
||||
required
|
||||
:disabled="loading"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-row">
|
||||
<NcTextField
|
||||
v-model="currency.api_endpoint"
|
||||
:label="strings.apiEndpoint"
|
||||
type="url"
|
||||
:placeholder="strings.apiEndpointPlaceholder"
|
||||
required
|
||||
:disabled="loading"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="field-row">
|
||||
<NcTextField
|
||||
v-model="currency.api_key"
|
||||
:label="strings.apiKey"
|
||||
type="password"
|
||||
:placeholder="strings.apiKeyPlaceholder"
|
||||
:disabled="loading"
|
||||
/>
|
||||
<NcTextField
|
||||
v-model="currency.json_path"
|
||||
:label="strings.jsonPath"
|
||||
:placeholder="strings.jsonPathPlaceholder"
|
||||
required
|
||||
:disabled="loading"
|
||||
/>
|
||||
</div>
|
||||
<NcButton
|
||||
type="error"
|
||||
@click="removeCurrency(index)"
|
||||
:disabled="loading"
|
||||
:aria-label="strings.deleteCurrency"
|
||||
>
|
||||
<template #icon>
|
||||
<Delete :size="20" />
|
||||
</template>
|
||||
</NcButton>
|
||||
</div>
|
||||
|
||||
<NcButton
|
||||
type="error"
|
||||
@click="removeCurrency(index)"
|
||||
:disabled="loading"
|
||||
:aria-label="strings.deleteCurrency"
|
||||
>
|
||||
<NcButton @click="addCurrency" :disabled="loading">
|
||||
<template #icon>
|
||||
<Delete :size="20" />
|
||||
<Plus :size="20" />
|
||||
</template>
|
||||
{{ strings.addCurrency }}
|
||||
</NcButton>
|
||||
</div>
|
||||
|
||||
<NcButton @click="addCurrency" :disabled="loading">
|
||||
<template #icon>
|
||||
<Plus :size="20" />
|
||||
</template>
|
||||
{{ strings.addCurrency }}
|
||||
</NcButton>
|
||||
</div>
|
||||
|
||||
<div class="submit-buttons">
|
||||
<NcButton type="primary" @click="saveCustomCurrencies" :disabled="loading">
|
||||
{{ strings.save }}
|
||||
</NcButton>
|
||||
<div class="submit-buttons">
|
||||
<NcButton type="primary" @click="saveCustomCurrencies" :disabled="loading">
|
||||
{{ strings.save }}
|
||||
</NcButton>
|
||||
</div>
|
||||
</div>
|
||||
</NcAppSettingsSection>
|
||||
|
||||
<NcAppSettingsSection id="cron-settings" :name="strings.cronSettingsHeader">
|
||||
<section>
|
||||
<form @submit.prevent="save">
|
||||
<div class="cron-flex">
|
||||
<NcSelect
|
||||
v-model="interval"
|
||||
:options="intervals"
|
||||
:input-label="strings.intervalLabel"
|
||||
required
|
||||
:disabled="loading"
|
||||
/>
|
||||
<div class="settings-section">
|
||||
<div class="cron-flex">
|
||||
<NcSelect
|
||||
v-model="interval"
|
||||
:options="intervals"
|
||||
:input-label="strings.intervalLabel"
|
||||
required
|
||||
:disabled="loading"
|
||||
/>
|
||||
|
||||
<div class="cron-last-update-container">
|
||||
<NcButton @click="doCron" :disabled="loading">{{ strings.fetchNow }}</NcButton>
|
||||
<div class="cron-last-update-container">
|
||||
<NcButton @click="doCron" :disabled="loading">{{ strings.fetchNow }}</NcButton>
|
||||
|
||||
<div>
|
||||
{{ strings.lastFetched }}
|
||||
<span v-if="loading">{{ strings.loading }}</span>
|
||||
<span v-if="!loading && !lastUpdate">{{ strings.never }}</span>
|
||||
<NcDateTime v-if="!loading && lastUpdate" :timestamp="lastUpdate.valueOf()" />
|
||||
<div>
|
||||
{{ strings.lastFetched }}
|
||||
<span v-if="loading">{{ strings.loading }}</span>
|
||||
<span v-if="!loading && !lastUpdate">{{ strings.never }}</span>
|
||||
<NcDateTime v-if="!loading && lastUpdate" :timestamp="lastUpdate.valueOf()" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="submit-buttons">
|
||||
<NcButton type="submit">{{ strings.save }}</NcButton>
|
||||
<div class="retention-field">
|
||||
<NcTextField
|
||||
v-model="retentionDays"
|
||||
type="number"
|
||||
:label="strings.retentionDaysLabel"
|
||||
:helper-text="strings.retentionDaysHelp"
|
||||
min="0"
|
||||
required
|
||||
:disabled="loading"
|
||||
/>
|
||||
</div>
|
||||
<div class="submit-buttons">
|
||||
<NcButton type="submit">{{ strings.save }}</NcButton>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
@@ -154,6 +169,7 @@ export default {
|
||||
return {
|
||||
loading: true,
|
||||
interval: null,
|
||||
retentionDays: 30,
|
||||
lastUpdate: null,
|
||||
customCurrencies: [],
|
||||
originalCustomCurrencies: [],
|
||||
@@ -192,6 +208,7 @@ export default {
|
||||
addCurrency: t(APP_ID, 'Add Currency'),
|
||||
deleteCurrency: t(APP_ID, 'Delete Currency'),
|
||||
cronSettingsHeader: t(APP_ID, 'Cron Settings'),
|
||||
intervalLabel: t(APP_ID, 'Update Interval'),
|
||||
instructionsHelp: t(
|
||||
APP_ID,
|
||||
'See the {aStart}Personal settings{aEnd} to view instructions on how to set up your currencies.',
|
||||
@@ -207,6 +224,11 @@ export default {
|
||||
loading: t(APP_ID, 'Loading…'),
|
||||
never: t(APP_ID, 'Never'),
|
||||
save: t(APP_ID, 'Save'),
|
||||
retentionDaysLabel: t(APP_ID, 'History Retention (days)'),
|
||||
retentionDaysHelp: t(
|
||||
APP_ID,
|
||||
'Number of days to keep currency history. Set to 0 for no limit (default: 30)',
|
||||
),
|
||||
},
|
||||
}
|
||||
},
|
||||
@@ -231,6 +253,10 @@ export default {
|
||||
console.warn('Invalid interval value', data.interval)
|
||||
}
|
||||
|
||||
if (data.retention_days !== undefined) {
|
||||
this.retentionDays = data.retention_days
|
||||
}
|
||||
|
||||
if (data.last_update) {
|
||||
const lastUpdate = parseDate(data.last_update, new Date())
|
||||
this.lastUpdate = lastUpdate
|
||||
@@ -259,7 +285,13 @@ export default {
|
||||
try {
|
||||
this.loading = true
|
||||
const interval = this.getIntervalByLabel(this.interval)?.value ?? 24
|
||||
const resp = await ocs.put('/settings', { data: { interval } })
|
||||
const retentionDays = this.retentionDays ?? 30
|
||||
const resp = await ocs.put('/settings', {
|
||||
data: {
|
||||
interval,
|
||||
retention_days: retentionDays,
|
||||
},
|
||||
})
|
||||
const data = resp.data
|
||||
this.loading = false
|
||||
console.debug('[DEBUG] Auto Currency settings saved', data)
|
||||
@@ -347,10 +379,6 @@ export default {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.submit-buttons {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.cron-flex {
|
||||
display: flex;
|
||||
align-items: start;
|
||||
@@ -395,5 +423,16 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.settings-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32px;
|
||||
}
|
||||
|
||||
.retention-field {
|
||||
max-width: 300px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user